open-claude-p 1.0.0
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.
- package/LICENSE +21 -0
- package/README.ja.md +708 -0
- package/README.ko.md +713 -0
- package/README.md +850 -0
- package/README.zh.md +708 -0
- package/bin/cli.js +782 -0
- package/package.json +68 -0
- package/scripts/postinstall.js +60 -0
- package/src/chat/event-filters.js +116 -0
- package/src/chat/index.js +1225 -0
- package/src/completion/detector.js +163 -0
- package/src/daemon/client.js +172 -0
- package/src/daemon/server.js +267 -0
- package/src/daemon/socket.js +78 -0
- package/src/index.js +908 -0
- package/src/options/index.js +4 -0
- package/src/options/parse-argv.js +214 -0
- package/src/options/spec.js +519 -0
- package/src/options/validate.js +104 -0
- package/src/output/index.js +8 -0
- package/src/output/json.js +83 -0
- package/src/output/registry.js +35 -0
- package/src/output/stream-json.js +111 -0
- package/src/output/text.js +94 -0
- package/src/parsers/ansi-strip.js +94 -0
- package/src/parsers/index.js +8 -0
- package/src/parsers/pipeline.js +50 -0
- package/src/parsers/registry.js +43 -0
- package/src/parsers/sentinel.js +41 -0
- package/src/parsers/tui-frame.js +256 -0
- package/src/print-mode.js +214 -0
- package/src/pty/index.js +3 -0
- package/src/pty/pool.js +127 -0
- package/src/pty/session.js +88 -0
- package/src/session-log.js +124 -0
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "open-claude-p",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A PTY-backed compatibility shim for `claude -p` (Claude Code headless/print mode). Drives the interactive `claude` CLI via node-pty and exposes the same option surface and stream-json contract. Ships as both a library and an `ocp` CLI binary.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./chat": "./src/chat/index.js",
|
|
10
|
+
"./options": "./src/options/index.js",
|
|
11
|
+
"./parsers": "./src/parsers/index.js",
|
|
12
|
+
"./output": "./src/output/index.js",
|
|
13
|
+
"./pty": "./src/pty/index.js"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"ocp": "bin/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"bin",
|
|
21
|
+
"scripts/postinstall.js",
|
|
22
|
+
"README.md",
|
|
23
|
+
"README.ko.md",
|
|
24
|
+
"README.ja.md",
|
|
25
|
+
"README.zh.md",
|
|
26
|
+
"LICENSE"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "node --test test/**/*.test.js",
|
|
30
|
+
"postinstall": "node scripts/postinstall.js",
|
|
31
|
+
"prepublishOnly": "npm test",
|
|
32
|
+
"pack:check": "npm pack --dry-run"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"claude",
|
|
36
|
+
"claude-code",
|
|
37
|
+
"claude-p",
|
|
38
|
+
"pty",
|
|
39
|
+
"node-pty",
|
|
40
|
+
"headless",
|
|
41
|
+
"cli",
|
|
42
|
+
"compatibility-shim"
|
|
43
|
+
],
|
|
44
|
+
"author": "Lee Yongbeom <top6616@gmail.com>",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"homepage": "https://github.com/empty-user77/open-claude-p#readme",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "git+https://github.com/empty-user77/open-claude-p.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/empty-user77/open-claude-p/issues"
|
|
53
|
+
},
|
|
54
|
+
"funding": {
|
|
55
|
+
"type": "github",
|
|
56
|
+
"url": "https://github.com/sponsors/empty-user77"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"node-pty": "^1.1.0",
|
|
63
|
+
"proper-lockfile": "^4.1.2"
|
|
64
|
+
},
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//
|
|
3
|
+
// Postinstall fixup for node-pty prebuilds.
|
|
4
|
+
//
|
|
5
|
+
// npm sometimes drops the execute bit on prebuilt helper binaries during tar
|
|
6
|
+
// extraction. node-pty ships a `spawn-helper` per (platform, arch) that MUST
|
|
7
|
+
// be executable; without the +x bit every `posix_spawnp` call fails on macOS
|
|
8
|
+
// with the unhelpful message "posix_spawnp failed.".
|
|
9
|
+
//
|
|
10
|
+
// This script restores the execute bit for whichever prebuild matches the
|
|
11
|
+
// current platform/arch. It is a no-op everywhere else.
|
|
12
|
+
//
|
|
13
|
+
// Safe to run repeatedly; safe to fail (e.g. when node-pty is not installed
|
|
14
|
+
// yet in some lifecycle ordering).
|
|
15
|
+
|
|
16
|
+
import { chmod, lstat } from 'node:fs/promises';
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import process from 'node:process';
|
|
20
|
+
|
|
21
|
+
// Resolve via `require.resolve` against THIS script's location, not
|
|
22
|
+
// process.cwd(). The npm-install case happens to use cwd = package
|
|
23
|
+
// root and the relative path worked accidentally, but manual
|
|
24
|
+
// invocation from another cwd would have chmod'd the wrong tree (or
|
|
25
|
+
// nothing). With require.resolve the path is always the node-pty
|
|
26
|
+
// actually loaded by our package.
|
|
27
|
+
let nodePtyRoot;
|
|
28
|
+
try {
|
|
29
|
+
const req = createRequire(import.meta.url);
|
|
30
|
+
nodePtyRoot = path.dirname(req.resolve('node-pty/package.json'));
|
|
31
|
+
} catch {
|
|
32
|
+
// node-pty not installed yet (lifecycle ordering) — exit silently.
|
|
33
|
+
process.exit(0);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const targets = [
|
|
37
|
+
path.join(nodePtyRoot, 'prebuilds', `${process.platform}-${process.arch}`, 'spawn-helper'),
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
let fixedAny = false;
|
|
41
|
+
for (const full of targets) {
|
|
42
|
+
try {
|
|
43
|
+
const st = await lstat(full);
|
|
44
|
+
// Refuse to follow symlinks: a malicious package preinstall hook could
|
|
45
|
+
// replace the prebuild path with a symlink to a sensitive binary and
|
|
46
|
+
// gain mode 0755 on it. lstat + bail keeps us on a regular file.
|
|
47
|
+
if (st.isSymbolicLink() || !st.isFile()) continue;
|
|
48
|
+
await chmod(full, 0o755);
|
|
49
|
+
process.stdout.write(`postinstall: chmod 0755 ${path.relative(process.cwd(), full) || full}\n`);
|
|
50
|
+
fixedAny = true;
|
|
51
|
+
} catch {
|
|
52
|
+
// file not present for this (platform, arch) — fine, skip silently
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!fixedAny) {
|
|
57
|
+
// Quiet success: in some environments (Windows, or pruned installs) none of
|
|
58
|
+
// the targets exist; that is not an error.
|
|
59
|
+
process.exit(0);
|
|
60
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Shared event-level cleanup helpers used by the CLI text adapter, the
|
|
2
|
+
// chat SDK, and the bundled sample server. Centralising them here keeps
|
|
3
|
+
// the "what counts as TUI noise vs. real content" decision in one
|
|
4
|
+
// place — when claude's TUI changes shape, a single edit propagates
|
|
5
|
+
// everywhere.
|
|
6
|
+
//
|
|
7
|
+
// Imported by:
|
|
8
|
+
// - src/output/text.js (CLI live spinner)
|
|
9
|
+
// - sample/server.js (SSE chat stream)
|
|
10
|
+
// - chat.send() (when `filterNoise: true`)
|
|
11
|
+
|
|
12
|
+
// Forward-declared so cleanSpinnerLabel can use it before the function
|
|
13
|
+
// definition below — keeps the export ordering readable.
|
|
14
|
+
const SPINNER_ICON_PREFIX = /^[✻✶✺✹✸✷✵●✢✳✽·✾✿❀❁❂❃❄❅❆❇❈❉❊❋]\s*/;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert an upstream `spinner` event label into a user-facing line of
|
|
18
|
+
* progress text. Returns `null` if the label is pure noise (token
|
|
19
|
+
* counters, lone digits, empty after trim, etc.).
|
|
20
|
+
*
|
|
21
|
+
* Examples:
|
|
22
|
+
* "✻ Brewing… (2s · 47 tokens)" → "Brewing…"
|
|
23
|
+
* "✻ Deliberating… 10s 23 still" → "Deliberating…"
|
|
24
|
+
* "✻ thinking" → null
|
|
25
|
+
* "✻" → null
|
|
26
|
+
*
|
|
27
|
+
* @param {string} label
|
|
28
|
+
* @returns {string|null}
|
|
29
|
+
*/
|
|
30
|
+
export function cleanSpinnerLabel(label) {
|
|
31
|
+
let s = String(label).replace(SPINNER_ICON_PREFIX, '').trim();
|
|
32
|
+
s = s
|
|
33
|
+
.replace(/\s{2,}\d.*$/, '') // " 10s ..." time/token stats
|
|
34
|
+
.replace(/\s{3,}.*$/, '') // TUI suffix after 3+ spaces
|
|
35
|
+
.replace(/\s+[·↓↑].*$/, '') // " · N tokens …" separator
|
|
36
|
+
.replace(/\s*\(.*$/, '') // "(stats)" suffix
|
|
37
|
+
.replace(/[\s\d·↓↑]+$/, '') // trailing junk
|
|
38
|
+
.trim();
|
|
39
|
+
if (!s) return null;
|
|
40
|
+
if (/^(thinking|still|tokens?)$/i.test(s)) return null;
|
|
41
|
+
if (/tokens[\)·]/.test(s) || /^\d/.test(s)) return null;
|
|
42
|
+
if (!/\S{3,}/.test(s)) return null;
|
|
43
|
+
// Final defensive strip — claude's spinner text reaches the user's
|
|
44
|
+
// terminal as live progress; an exotic upstream that smuggled an
|
|
45
|
+
// ANSI sequence into the label would otherwise execute it.
|
|
46
|
+
return stripTerminalControl(s, 128);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `true` if an `assistant-text` event payload is TUI chrome rather than
|
|
51
|
+
* model output — TUI prompt characters, box borders, redraw artifacts,
|
|
52
|
+
* tree-view glyphs, "Found N" search status lines, status indicators,
|
|
53
|
+
* and tool-call announcements that other systems should not surface as
|
|
54
|
+
* assistant content.
|
|
55
|
+
*
|
|
56
|
+
* @param {string} text
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function isAssistantTextNoise(text) {
|
|
60
|
+
const t = String(text ?? '');
|
|
61
|
+
if (!t) return true;
|
|
62
|
+
if (/^\s*[❯›❮‹✳✽·✾✿❀❁❂❃❄]\s*$/.test(t)) return true; // lone TUI glyph
|
|
63
|
+
if (/^[\s\d\r\n]+$/.test(t)) return true; // whitespace / digits only
|
|
64
|
+
if (/⎿/.test(t)) return true; // tree-view marker
|
|
65
|
+
if (/MCP servers? failed/i.test(t)) return true; // MCP status line
|
|
66
|
+
if (/^\s*(↑|↓|·|still running)\s*$/i.test(t)) return true; // arrow / indicator
|
|
67
|
+
if (/^(Found \d+|Did \d+)\b/.test(t.trim())) return true; // search status
|
|
68
|
+
if (/^\s*(?:Web ?Search|WebFetch|Bash|Read|Write|Glob|Grep|LS|Edit|MultiEdit|TodoWrite|TodoRead|mcp__\w+)\(/.test(t)) return true; // tool-call announcement
|
|
69
|
+
// "Inferring… 8 word" — spinner + digit + word, pure animation frame
|
|
70
|
+
if (/\w(?:…|\.{2,3})\s{3,}\S.*$/.test(t)) return true;
|
|
71
|
+
// Heavy leading indent + ≤2 words — usually a single-line spinner element
|
|
72
|
+
{ const tr = t.trim(); if ((t.length - tr.length) > 8 && tr.split(/\s+/).length <= 2) return true; }
|
|
73
|
+
// "Nucleating... 4 3 Nucleating... 5" — strip CapWord… tokens; if only digits/spaces remain it's pure spinner noise
|
|
74
|
+
if (/[A-Z][a-zA-Z]+[\.…]{2,3}/.test(t)) {
|
|
75
|
+
const rest = t.replace(/[A-Z][a-zA-Z\-]+[\.…]{2,3}/g, '').replace(/\s/g, '');
|
|
76
|
+
if (/^\d*$/.test(rest)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Strip control / escape characters that would execute as terminal
|
|
83
|
+
* sequences when echoed to a TTY. Use on any string sourced from
|
|
84
|
+
* untrusted-by-the-terminal channels (upstream JSONL `tool_use.name`,
|
|
85
|
+
* spinner labels, user prompt previews) before writing to stderr.
|
|
86
|
+
* Caps the length to keep meta-line / status-line wraps sane.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} s
|
|
89
|
+
* @param {number} [max=64]
|
|
90
|
+
* @returns {string}
|
|
91
|
+
*/
|
|
92
|
+
export function stripTerminalControl(s, max = 64) {
|
|
93
|
+
// Strip C0 controls + DEL + the full C1 8-bit control range. The
|
|
94
|
+
// narrower `\x9b` (CSI) class missed `\x90` (DCS), `\x9d` (OSC),
|
|
95
|
+
// `\x9e` (PM), `\x9f` (APC) — all of which execute on VT100-compatible
|
|
96
|
+
// terminals when 8-bit forms are accepted. Also strips ESC (`\x1b`)
|
|
97
|
+
// which is the 7-bit prefix for the same sequences.
|
|
98
|
+
return String(s ?? '')
|
|
99
|
+
.replace(/[\x00-\x1f\x7f\x80-\x9f]/g, '')
|
|
100
|
+
.slice(0, max);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Tool-call announcement pattern → first capture is the tool name. */
|
|
104
|
+
const TOOL_CALL_RE = /^\s*(Web Search|WebSearch|WebFetch|Bash|Read|Write|Glob|Grep|LS|Edit|MultiEdit|TodoWrite|TodoRead|mcp__[\w]+)\(/;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract the tool name from an `assistant-text` line that announces a
|
|
108
|
+
* tool call. Returns `null` if the line is not a tool announcement.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} text
|
|
111
|
+
* @returns {string|null}
|
|
112
|
+
*/
|
|
113
|
+
export function extractToolName(text) {
|
|
114
|
+
const m = String(text ?? '').match(TOOL_CALL_RE);
|
|
115
|
+
return m ? m[1] : null;
|
|
116
|
+
}
|