@dexh/shannon 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +31 -64
  2. package/index.ts +23 -15
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,72 +1,43 @@
1
1
  # Shannon
2
2
 
3
- Shannon is a CLI and SDK wrapper around the interactive Claude Code CLI. It starts a real `claude` session inside tmux, sends a prompt, tails Claude's on-disk transcript, emits stream JSON, and cleans up the tmux session on exit.
4
-
5
- The first supported path is:
6
-
7
- ```sh
8
- shannon -p "hi i'm tom" --output-format=stream-json --verbose
9
- ```
10
-
11
- Shannon does not use `claude -p` internally.
3
+ Shannon is a CLI and SDK wrapper around the interactive Claude Code CLI. It runs a real `claude` session inside tmux, sends a prompt, and emits stream JSON.
12
4
 
13
5
  ## Requirements
14
6
 
15
- - Bun
7
+ - [Bun](https://bun.sh)
16
8
  - `claude` on `PATH`
17
9
  - `tmux` on `PATH`
18
- - A working Claude Code login/configuration
10
+ - A working Claude Code login
19
11
 
20
- ## Install
21
-
22
- ```sh
23
- bun install
24
- bun link
25
- ```
12
+ ## CLI
26
13
 
27
- After linking, `shannon` should resolve from your Bun bin directory:
14
+ Run without installing:
28
15
 
29
16
  ```sh
30
- which shannon
17
+ npx @dexh/shannon -p "Reply with exactly: hello" --output-format=stream-json --verbose
31
18
  ```
32
19
 
33
- ## CLI
20
+ Or install globally:
34
21
 
35
22
  ```sh
23
+ npm install -g @dexh/shannon
36
24
  shannon -p "Reply with exactly: hello" --output-format=stream-json --verbose
37
25
  ```
38
26
 
39
- Incremental stdin JSONL input is supported for one or more user messages:
27
+ Output formats: `stream-json` (JSONL), `json` (single array), `text` (final result text).
28
+
29
+ ## SDK
40
30
 
41
31
  ```sh
42
- printf '%s\n' '{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Reply with exactly: hello"}]},"parent_tool_use_id":null,"session_id":""}' \
43
- | shannon --input-format=stream-json --output-format=stream-json --verbose --replay-user-messages
32
+ npm install @dexh/shannon
44
33
  ```
45
34
 
46
- Current stream shape:
47
-
48
- - `system` / `hook_response` when interactive transcript hook attachments are present
49
- - `system` / `init`
50
- - `assistant`
51
- - synthesized `result` / `success`
52
- - final `shannon_session` / `metadata`
53
-
54
- The final metadata row includes the Claude session id, transcript path, project session folder, tmux session name, cwd, and cleanup status.
55
-
56
- `--output-format=json` emits one JSON array containing Shannon's supported
57
- message rows. `--output-format=text` emits the final result text.
58
-
59
- ## SDK
60
-
61
35
  ```ts
62
- import { query } from "@humanlayer/shannon";
36
+ import { query } from "@dexh/shannon";
63
37
 
64
38
  for await (const message of query({
65
39
  prompt: "Reply with exactly: hello",
66
- options: {
67
- outputFormat: "stream-json",
68
- verbose: true,
69
- },
40
+ options: { outputFormat: "stream-json", verbose: true },
70
41
  })) {
71
42
  console.log(JSON.stringify(message));
72
43
  }
@@ -75,7 +46,7 @@ for await (const message of query({
75
46
  Async input is also accepted for finite user-message streams:
76
47
 
77
48
  ```ts
78
- import { query, type ShannonUserMessage } from "@humanlayer/shannon";
49
+ import { query, type ShannonUserMessage } from "@dexh/shannon";
79
50
 
80
51
  async function* messages(): AsyncIterable<ShannonUserMessage> {
81
52
  yield {
@@ -94,36 +65,32 @@ for await (const message of query({ prompt: messages() })) {
94
65
  }
95
66
  ```
96
67
 
97
- Queries accept an `AbortController` in options; aborting it terminates the
98
- underlying Shannon subprocess.
68
+ Pass an `AbortController` in options to terminate the underlying Shannon subprocess.
99
69
 
100
- The SDK also exports zod schemas for the current Shannon message, options, and
101
- query parameter surface:
70
+ ## Agent SDK facade
102
71
 
103
- ```ts
104
- import { shannonMessageSchema, shannonQueryOptionsSchema } from "@humanlayer/shannon";
72
+ `@dexh/shannon-agent-sdk` is a Claude Agent SDK-compatible facade that re-exports Shannon's SDK surface. Full parity is a work in progress (see `GOAL_PROGRESS.md`).
73
+
74
+ ```sh
75
+ npm install @dexh/shannon-agent-sdk
105
76
  ```
106
77
 
107
- The SDK facade shells out to the `shannon` executable and parses JSONL stdout into an async iterable.
78
+ ```ts
79
+ import { query } from "@dexh/shannon-agent-sdk";
108
80
 
109
- The repo also includes `packages/shannon-agent-sdk`, a thin
110
- `@humanlayer/shannon-agent-sdk` facade over the implemented Shannon SDK surface.
111
- It is not full Claude Agent SDK parity yet.
81
+ for await (const message of query({
82
+ prompt: "Reply with exactly: hello",
83
+ options: { outputFormat: "stream-json", verbose: true },
84
+ })) {
85
+ console.log(JSON.stringify(message));
86
+ }
87
+ ```
112
88
 
113
89
  ## Development
114
90
 
115
91
  ```sh
92
+ bun install
116
93
  bun test
117
94
  bun run typecheck
118
- ```
119
-
120
- CI runs the same non-live checks plus package dry-runs for both packages.
121
- The publish workflow publishes `@humanlayer/shannon` and
122
- `@humanlayer/shannon-agent-sdk` from a GitHub release or manual dispatch when
123
- `NPM_TOKEN` is configured.
124
-
125
- Run the CLI directly:
126
-
127
- ```sh
128
95
  bun ./index.ts -p "hello" --output-format=stream-json --verbose
129
96
  ```
package/index.ts CHANGED
@@ -629,10 +629,10 @@ export async function runShannon(options: CliOptions) {
629
629
  const prompts = options.prompt
630
630
  ? asyncIterableFromArray([options.prompt])
631
631
  : readPromptsFromStdin(options.inputFormat);
632
+ const promptIterator = prompts[Symbol.asyncIterator]();
632
633
  let meta: SessionMetadata | undefined;
633
634
  let transcriptRowCount = 0;
634
635
  let cleanup: JsonRecord = { tmux_killed: false };
635
- let promptReady = false;
636
636
  let promptCount = 0;
637
637
  let cleanupStarted = false;
638
638
  let metadataEmitted = false;
@@ -657,6 +657,12 @@ export async function runShannon(options: CliOptions) {
657
657
  });
658
658
 
659
659
  try {
660
+ let prompt = await nextPrompt(promptIterator);
661
+ if (!prompt) {
662
+ throw new Error("Expected at least one user message on stdin for --input-format=stream-json");
663
+ }
664
+
665
+ const firstPromptSentAt = Date.now();
660
666
  await runCommand([
661
667
  "tmux",
662
668
  "new-session",
@@ -667,27 +673,23 @@ export async function runShannon(options: CliOptions) {
667
673
  options.cwd,
668
674
  "claude",
669
675
  ...options.claudeArgs,
676
+ prompt,
670
677
  ]);
671
- await waitForPrompt(tmuxSession);
672
- promptReady = true;
673
678
 
674
- for await (const prompt of prompts) {
675
- if (!prompt) continue;
679
+ let launchedWithPrompt = true;
680
+ while (prompt) {
676
681
  promptCount += 1;
677
682
 
678
- if (!promptReady) {
683
+ const promptSentAt = launchedWithPrompt ? firstPromptSentAt : Date.now();
684
+ if (!launchedWithPrompt) {
679
685
  await waitForPrompt(tmuxSession);
680
- promptReady = true;
686
+ await sendPrompt(tmuxSession, prompt);
681
687
  }
682
688
 
683
689
  if (options.replayUserMessages && options.outputFormat === "stream-json") {
684
690
  emitJson(toUserReplay(prompt));
685
691
  }
686
692
 
687
- const promptSentAt = Date.now();
688
- await sendPrompt(tmuxSession, prompt);
689
- promptReady = false;
690
-
691
693
  if (!meta) {
692
694
  const discovery = await waitForSessionWithPrompt(
693
695
  projectFolder,
@@ -741,10 +743,9 @@ export async function runShannon(options: CliOptions) {
741
743
  } else {
742
744
  emitOutput(options.outputFormat, turnMessages);
743
745
  }
744
- }
745
746
 
746
- if (promptCount === 0) {
747
- throw new Error("Expected at least one user message on stdin for --input-format=stream-json");
747
+ launchedWithPrompt = false;
748
+ prompt = await nextPrompt(promptIterator);
748
749
  }
749
750
 
750
751
  if (options.outputFormat === "json") {
@@ -925,10 +926,17 @@ async function waitForPrompt(tmuxSession: string) {
925
926
  async function sendPrompt(tmuxSession: string, prompt: string) {
926
927
  await runCommand(["tmux", "set-buffer", "-b", `shannon-${tmuxSession}`, prompt]);
927
928
  await runCommand(["tmux", "paste-buffer", "-b", `shannon-${tmuxSession}`, "-t", tmuxSession]);
928
- await runCommand(["tmux", "send-keys", "-t", tmuxSession, "Escape"]);
929
929
  await runCommand(["tmux", "send-keys", "-t", tmuxSession, "C-m"]);
930
930
  }
931
931
 
932
+ async function nextPrompt(iterator: AsyncIterator<string>): Promise<string | undefined> {
933
+ while (true) {
934
+ const next = await iterator.next();
935
+ if (next.done) return undefined;
936
+ if (next.value) return next.value;
937
+ }
938
+ }
939
+
932
940
  async function waitForAssistantReply(
933
941
  transcriptPath: string,
934
942
  prompt: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dexh/shannon",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "CLI and SDK wrapper that drives interactive Claude Code through tmux and emits stream-json.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/humanlayer/shannon#readme",