npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@trylooper/sdk

v0.2.0

Published

Looper Agent SDK: drive the Looper agent (the looper CLI) from Node.js — streaming events, tool approvals, sessions shared with the Looper app.

Readme

Looper Agent SDK

Drive the Looper Agent from Node.js. It is the same agent as the Looper desktop app and the looper CLI: its tools, sub-agents, Auto approvals, compaction, /goal, checkpoints and MCP. The SDK runs the native looper binary (installed with it) and speaks its stream-json protocol.

npm i @trylooper/sdk

Supported on macOS (arm64, x64) and Linux (x64, arm64), with Node 18 or newer. The binary comes with the SDK; you do not need to install the CLI. The SDK looks for it in options.pathToLooperExecutable, then $LOOPER_BIN, then the platform package installed with it, then a looper on the PATH (for example one from brew install luizguil99/tap/looper).

Quick start

import { query } from '@trylooper/sdk'

for await (const msg of query({
  prompt: 'Fix the failing test',
  options: { cwd: '/path/to/repo', model: 'anthropic/claude-sonnet-5-5', permissionMode: 'acceptEdits' },
})) {
  if (msg.type === 'event' && msg.event.type === 'text') process.stdout.write(msg.event.content)
  if (msg.type === 'result') console.log('\n', msg.subtype, msg.usage)
}

Provider keys come from looper auth set … (shared with the Looper app), ANTHROPIC_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY, or options.secrets. Secrets are passed through a file that the CLI deletes, so they never appear on the command line.

Live code and terminal streaming

Each tool card shows two live streams, as in the Looper app:

  • Code. While the model is writing a Write or Edit call, tool_use events with the same id carry the code written so far, about every 100 ms.
  • Terminal. A running Shell command sends its stdout and stderr as bash_output chunks.

For the code, there are two granularities:

  • Default: drafts of the code so far, about every 100 ms. This is what the Looper app shows.
  • includePartialToolInput: true: every chunk the model writes arrives as it is generated, as a tool_input_delta event with the raw JSON of the arguments. This works for any tool and matches the Claude Agent SDK's input_json_delta stream events with includePartialMessages. With a real model, writing a 60-line file gave about 120 updates of ~16 characters, against ~20 drafts.

ToolCalls turns these events into updates. It decodes the raw JSON exactly, escapes included, and gives the same code updates in both modes:

import { query, ToolCalls } from '@trylooper/sdk'

const tools = new ToolCalls()
for await (const msg of query({
  prompt: 'Add a /health route and run the tests',
  options: { cwd, includePartialToolInput: true },
})) {
  for (const u of tools.push(msg)) {
    switch (u.kind) {
      case 'start':  console.log(`\n▸ ${u.call.tool}`); break
      case 'code':   process.stdout.write(u.delta); break        // u.field: contents | old_string | new_string
      case 'input_delta': break                                  // raw JSON chunk of any tool (u.partialJson)
      case 'output': process.stdout.write(u.chunk); break        // u.stream: stdout | stderr
      case 'result': console.log(`\n✓ ${u.call.tool}`, u.result.isError ? 'failed' : ''); break
      case 'file':   console.log(`edited ${u.modification.filePath}`); break
    }
  }
}

tools.get(id) returns the current state of a call: its merged input, its status, the terminal output / outputText, the result (whose data has the diff of a file tool) and the fileModification (content before and after).

Approvals

const q = query({
  prompt: 'Clean the build folder',
  options: {
    cwd,
    permissionMode: 'ask',
    canUseTool: async (req, { signal }) =>
      req.tool_name === 'Shell' && req.command?.startsWith('rm ')
        ? { behavior: 'deny', message: 'No deletions in CI' }
        : { behavior: 'allow', scope: 'once' },          // scope: 'session' allows the tool for the session
  },
})

Without canUseTool, the permission mode decides alone: anything that would ask is denied unless permissionMode is bypass. The SDK keeps the session's input open until it is idle, so approvals from background sub-agents still reach canUseTool after the turn's result.

Streaming input and control

async function* messages() {
  yield 'Read the README'
  yield { content: 'Now summarize it in Portuguese', model: 'anthropic/claude-haiku-5-5' }
}
const q = query({ prompt: messages(), options: { cwd } })

q.send({ content: 'also check the tests', priority: 'now' })   // steer the running turn
await q.interrupt()          // stop the running turn; queued messages then run
await q.stop()               // the app's Stop button: also drops queued messages
await q.setModel('chatgpt/gpt-6.1-sol'); await q.setPermissionMode('auto'); await q.setAgentMode('plan')
await q.listModels(); await q.getContextUsage(); await q.mcpStatus(); await q.listBackground()
await q.stopTask(taskId); await q.compact()
await q.checkpointPreview({ index: 2 }); await q.checkpointRestore({ index: 2 }); await q.checkpointRedo()
await q.resend({ index: 2 }, 'the edited text')   // edit & resend (no text = retry); truncateMessages() alone cuts
await q.forkSession({ index: 3 }); await q.renameSession('Login fix')
await q.listFileModifications(); await q.acceptFileModifications(); await q.revertFileModifications(path)
await q.userInputResponse(card.id, { vars: [{ key: 'API_KEY', value }] })   // a user_input_request form (Looper.app running)
(await q.initializationResult()).commands   // slash commands, tools, capabilities
q.close()                    // terminate now

A steer (priority: 'now') the turn never read comes back as a user_steer_dropped event with its uuid. Without stop() the CLI then sends it as its own turn; after stop() it does not: re-send it yourself if you want it (the session no longer waits for its result).

Options: cwd, model, effort, agentMode (agent | plan | ask | multitask), permissionMode (default | auto | ask | acceptEdits | bypass), canUseTool, sessionId / continue (resume a chat, including one started in the Looper app), appendSystemPrompt, allowedTools / disallowedTools, includePartialToolInput, dbPath / standalone, background (wait | detach), secrets, env, abortController, stderr, pathToLooperExecutable, extraArgs.

Messages

The iterator yields:

  • system/init: the session, model, tools and capabilities;
  • event: one agent event (text and reasoning deltas, tool_use, tool_result, bash_output, file_modification, usage, approval_request, async_question, plan_approval_request, …);
  • result: one per turn;
  • system/task_started and system/task_notification: background sub-agents and shells;
  • system/turn_started: follow-up and resumed turns;
  • system/status, compact_boundary and goal.

Every type is exported (LooperMessage, AgentEvent, EventOf<'tool_use'>, …). They are generated from the protocol's JSON Schema. The helpers isEvent(msg, 'text') and collectText(messages) cover common cases.

Docs: https://trylooper.app/cli