@zhihaohong52/sonata 0.4.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.md +437 -0
- package/dist/adapters/claude.js +83 -0
- package/dist/adapters/claude.js.map +1 -0
- package/dist/adapters/codex.js +382 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/opencode.js +104 -0
- package/dist/adapters/opencode.js.map +1 -0
- package/dist/adapters/pi.js +152 -0
- package/dist/adapters/pi.js.map +1 -0
- package/dist/adapters/reasonix.js +358 -0
- package/dist/adapters/reasonix.js.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/agent-markers.js +3 -0
- package/dist/agent-markers.js.map +1 -0
- package/dist/aipricing.js +109 -0
- package/dist/aipricing.js.map +1 -0
- package/dist/catalog.js +274 -0
- package/dist/catalog.js.map +1 -0
- package/dist/cli.js +766 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/approve.js +23 -0
- package/dist/commands/approve.js.map +1 -0
- package/dist/commands/auth.js +41 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/commands/catalog.js +172 -0
- package/dist/commands/catalog.js.map +1 -0
- package/dist/commands/code.js +113 -0
- package/dist/commands/code.js.map +1 -0
- package/dist/commands/dispatch.js +148 -0
- package/dist/commands/dispatch.js.map +1 -0
- package/dist/commands/doctor.js +545 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/gc.js +25 -0
- package/dist/commands/gc.js.map +1 -0
- package/dist/commands/init-log.js +83 -0
- package/dist/commands/init-log.js.map +1 -0
- package/dist/commands/init.js +157 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/log.js +24 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/route.js +660 -0
- package/dist/commands/route.js.map +1 -0
- package/dist/commands/run.js +199 -0
- package/dist/commands/run.js.map +1 -0
- package/dist/commands/runs.js +37 -0
- package/dist/commands/runs.js.map +1 -0
- package/dist/commands/serve.js +818 -0
- package/dist/commands/serve.js.map +1 -0
- package/dist/commands/status.js +14 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/sync.js +258 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/tail.js +229 -0
- package/dist/commands/tail.js.map +1 -0
- package/dist/commands/usage.js +85 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/commands/verify.js +69 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/commands/wait.js +38 -0
- package/dist/commands/wait.js.map +1 -0
- package/dist/config.js +588 -0
- package/dist/config.js.map +1 -0
- package/dist/detect.js +445 -0
- package/dist/detect.js.map +1 -0
- package/dist/filelock.js +83 -0
- package/dist/filelock.js.map +1 -0
- package/dist/init/apply.js +96 -0
- package/dist/init/apply.js.map +1 -0
- package/dist/init/candidates.js +115 -0
- package/dist/init/candidates.js.map +1 -0
- package/dist/init/discover.js +216 -0
- package/dist/init/discover.js.map +1 -0
- package/dist/init/helpers.js +429 -0
- package/dist/init/helpers.js.map +1 -0
- package/dist/init/interactive-state.js +128 -0
- package/dist/init/interactive-state.js.map +1 -0
- package/dist/init/plan.js +225 -0
- package/dist/init/plan.js.map +1 -0
- package/dist/init/scripted-state.js +130 -0
- package/dist/init/scripted-state.js.map +1 -0
- package/dist/init/toml.js +83 -0
- package/dist/init/toml.js.map +1 -0
- package/dist/init/validate.js +149 -0
- package/dist/init/validate.js.map +1 -0
- package/dist/ledger.js +123 -0
- package/dist/ledger.js.map +1 -0
- package/dist/mcp/protocol.js +64 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.js +96 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.js +210 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mode.js +64 -0
- package/dist/mode.js.map +1 -0
- package/dist/native/claude-shim.js +228 -0
- package/dist/native/claude-shim.js.map +1 -0
- package/dist/native/codex-auth.js +177 -0
- package/dist/native/codex-auth.js.map +1 -0
- package/dist/native/copilot-auth.js +105 -0
- package/dist/native/copilot-auth.js.map +1 -0
- package/dist/native/credentials.js +83 -0
- package/dist/native/credentials.js.map +1 -0
- package/dist/native/hosts.js +60 -0
- package/dist/native/hosts.js.map +1 -0
- package/dist/native/litellm.js +103 -0
- package/dist/native/litellm.js.map +1 -0
- package/dist/native/models.js +96 -0
- package/dist/native/models.js.map +1 -0
- package/dist/native/oauth-login.js +112 -0
- package/dist/native/oauth-login.js.map +1 -0
- package/dist/native/router.js +469 -0
- package/dist/native/router.js.map +1 -0
- package/dist/native/tls.js +73 -0
- package/dist/native/tls.js.map +1 -0
- package/dist/native/usage.js +119 -0
- package/dist/native/usage.js.map +1 -0
- package/dist/normalize.js +98 -0
- package/dist/normalize.js.map +1 -0
- package/dist/pricing.js +86 -0
- package/dist/pricing.js.map +1 -0
- package/dist/roles.js +49 -0
- package/dist/roles.js.map +1 -0
- package/dist/sessions.js +62 -0
- package/dist/sessions.js.map +1 -0
- package/dist/settings.js +131 -0
- package/dist/settings.js.map +1 -0
- package/dist/store.js +70 -0
- package/dist/store.js.map +1 -0
- package/dist/tmux.js +89 -0
- package/dist/tmux.js.map +1 -0
- package/dist/tui-ink/app-state.js +340 -0
- package/dist/tui-ink/app-state.js.map +1 -0
- package/dist/tui-ink/app.js +156 -0
- package/dist/tui-ink/app.js.map +1 -0
- package/dist/tui-ink/components/byok-step.js +102 -0
- package/dist/tui-ink/components/byok-step.js.map +1 -0
- package/dist/tui-ink/components/login-screen.js +80 -0
- package/dist/tui-ink/components/login-screen.js.map +1 -0
- package/dist/tui-ink/components/models-step.js +92 -0
- package/dist/tui-ink/components/models-step.js.map +1 -0
- package/dist/tui-ink/components/multi-select-state.js +56 -0
- package/dist/tui-ink/components/multi-select-state.js.map +1 -0
- package/dist/tui-ink/components/multi-select.js +69 -0
- package/dist/tui-ink/components/multi-select.js.map +1 -0
- package/dist/tui-ink/components/providers-step.js +285 -0
- package/dist/tui-ink/components/providers-step.js.map +1 -0
- package/dist/tui-ink/components/ranked-select-state.js +41 -0
- package/dist/tui-ink/components/ranked-select-state.js.map +1 -0
- package/dist/tui-ink/components/ranked-select.js +61 -0
- package/dist/tui-ink/components/ranked-select.js.map +1 -0
- package/dist/tui-ink/components/search-select.js +47 -0
- package/dist/tui-ink/components/search-select.js.map +1 -0
- package/dist/tui-ink/components/text-input-state.js +42 -0
- package/dist/tui-ink/components/text-input-state.js.map +1 -0
- package/dist/tui-ink/components/text-input.js +48 -0
- package/dist/tui-ink/components/text-input.js.map +1 -0
- package/dist/tui-ink/run.js +38 -0
- package/dist/tui-ink/run.js.map +1 -0
- package/dist/tui-ink/test-compile.js +4 -0
- package/dist/tui-ink/test-compile.js.map +1 -0
- package/dist/tui-ink/types.js +2 -0
- package/dist/tui-ink/types.js.map +1 -0
- package/dist/tui.js +330 -0
- package/dist/tui.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/watchdog.js +85 -0
- package/dist/watchdog.js.map +1 -0
- package/hooks/capture-mode.mjs +56 -0
- package/hooks/ensure-serve.mjs +140 -0
- package/hooks/hooks.json +12 -0
- package/hooks/route-session.mjs +47 -0
- package/hooks/route-subagent.mjs +54 -0
- package/package.json +64 -0
- package/roles/code.md +13 -0
- package/roles/explore.md +9 -0
- package/roles/plan.md +10 -0
- package/roles/review.md +11 -0
- package/skills/loop/SKILL.md +43 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { InitWizard } from './app.js';
|
|
4
|
+
/**
|
|
5
|
+
* Runs the Ink wizard and returns only once Ink has released stdin.
|
|
6
|
+
*
|
|
7
|
+
* The wait is the whole point. `unmount()` starts a teardown that finishes
|
|
8
|
+
* asynchronously — Ink restores raw mode, detaches its own reader and pauses
|
|
9
|
+
* the stream after it returns. Resolving straight after calling it therefore
|
|
10
|
+
* handed control back to `cmdInit` mid-teardown: the next prompt (`Install the
|
|
11
|
+
* permission hook`) attached its iterator to stdin, Ink's cleanup then ran and
|
|
12
|
+
* pulled the stream out from under it, and the prompt ended without ever
|
|
13
|
+
* receiving a keystroke. The run exited with the prompt still on screen and
|
|
14
|
+
* nothing written — which is what "sonata init never saves the config" was.
|
|
15
|
+
*
|
|
16
|
+
* `waitUntilExit()` resolves after that cleanup, so stdin is quiet and
|
|
17
|
+
* unclaimed by the time the next prompt asks for it.
|
|
18
|
+
*/
|
|
19
|
+
export async function runInitTui(data, log = () => { }) {
|
|
20
|
+
let outcome;
|
|
21
|
+
const instance = render(React.createElement(InitWizard, {
|
|
22
|
+
data,
|
|
23
|
+
onDone: (result) => {
|
|
24
|
+
outcome ??= result;
|
|
25
|
+
instance.unmount();
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
await instance.waitUntilExit();
|
|
29
|
+
if (outcome === undefined) {
|
|
30
|
+
// Ink ended without the wizard reporting anything — Ctrl-C, a signal, or a
|
|
31
|
+
// closed stream. Indistinguishable from a deliberate cancel in the result,
|
|
32
|
+
// and very distinguishable in a bug report, so say which one happened.
|
|
33
|
+
log('wizard: ink exited without a result (interrupt, signal or closed stdin)');
|
|
34
|
+
return { cancelled: true, state: data.initialState ?? {} };
|
|
35
|
+
}
|
|
36
|
+
return outcome;
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/tui-ink/run.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAE,UAAU,EAAmB,MAAM,UAAU,CAAC;AAGvD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,IAAgB,EAChB,MAA8B,GAAG,EAAE,GAAE,CAAC;IAEtC,IAAI,OAA8B,CAAC;IAEnC,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,aAAa,CAAC,UAAU,EAAE;QACtD,IAAI;QACJ,MAAM,EAAE,CAAC,MAAiB,EAAE,EAAE;YAC5B,OAAO,KAAK,MAAM,CAAC;YACnB,QAAQ,CAAC,OAAO,EAAE,CAAC;QACrB,CAAC;KACF,CAAC,CAAC,CAAC;IAEJ,MAAM,QAAQ,CAAC,aAAa,EAAE,CAAC;IAE/B,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,2EAA2E;QAC3E,2EAA2E;QAC3E,uEAAuE;QACvE,GAAG,CAAC,yEAAyE,CAAC,CAAC;QAC/E,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,YAAY,IAAI,EAAE,EAAE,CAAC;IAC7D,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-compile.js","sourceRoot":"","sources":["../../src/tui-ink/test-compile.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAE3B,MAAM,CAAC,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC,KAAC,IAAI,wBAAa,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tui-ink/types.ts"],"names":[],"mappings":""}
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal zero-dependency TUI primitives.
|
|
3
|
+
*
|
|
4
|
+
* The interactive glue is deliberately thin: all list behaviour lives in the
|
|
5
|
+
* pure `parseKey` / `reduce` / `renderList` functions so it can be tested
|
|
6
|
+
* without a TTY. Only `runList` and `confirm` touch stdin.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* `filterable` decides what a letter means. In a filterable list it is text;
|
|
10
|
+
* in a plain list it is vim navigation. The lists that filter are the long
|
|
11
|
+
* ones, where typing is the only practical way to reach an entry.
|
|
12
|
+
*/
|
|
13
|
+
export function parseKey(seq, filterable) {
|
|
14
|
+
switch (seq) {
|
|
15
|
+
case '\u001b[A': return { kind: 'up' };
|
|
16
|
+
case '\u001b[B': return { kind: 'down' };
|
|
17
|
+
// Left steps back a screen. Free to claim even in a filterable list: the
|
|
18
|
+
// filter is append-only, with no cursor to move within it.
|
|
19
|
+
case '\u001b[D': return { kind: 'back' };
|
|
20
|
+
// Space toggles even while filtering: no provider or model ref contains
|
|
21
|
+
// one, so the filter never needs a space.
|
|
22
|
+
case ' ': return { kind: 'space' };
|
|
23
|
+
case '\r':
|
|
24
|
+
case '\n': return { kind: 'enter' };
|
|
25
|
+
case '\u0001': return { kind: 'toggleAll' };
|
|
26
|
+
case '\u0003':
|
|
27
|
+
case '\u001b': return { kind: 'cancel' };
|
|
28
|
+
case '\u007f':
|
|
29
|
+
case '\b': return { kind: 'backspace' };
|
|
30
|
+
default: break;
|
|
31
|
+
}
|
|
32
|
+
if (!filterable) {
|
|
33
|
+
if (seq === 'k')
|
|
34
|
+
return { kind: 'up' };
|
|
35
|
+
if (seq === 'j')
|
|
36
|
+
return { kind: 'down' };
|
|
37
|
+
return { kind: 'ignore' };
|
|
38
|
+
}
|
|
39
|
+
// eslint-disable-next-line no-control-regex
|
|
40
|
+
if (seq.length > 0 && !/[\u0000-\u001f\u007f]/.test(seq)) {
|
|
41
|
+
return { kind: 'char', value: seq };
|
|
42
|
+
}
|
|
43
|
+
return { kind: 'ignore' };
|
|
44
|
+
}
|
|
45
|
+
export function visibleIndices(choices, filter) {
|
|
46
|
+
const q = filter.trim().toLowerCase();
|
|
47
|
+
const out = [];
|
|
48
|
+
choices.forEach((c, i) => {
|
|
49
|
+
if (q.length === 0 || c.label.toLowerCase().includes(q))
|
|
50
|
+
out.push(i);
|
|
51
|
+
});
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
export function initialState(choices) {
|
|
55
|
+
const checked = new Set();
|
|
56
|
+
choices.forEach((c, i) => {
|
|
57
|
+
if (c.checked)
|
|
58
|
+
checked.add(i);
|
|
59
|
+
});
|
|
60
|
+
const firstEnabled = choices.findIndex((c) => !c.disabled);
|
|
61
|
+
return {
|
|
62
|
+
cursor: firstEnabled === -1 ? 0 : firstEnabled,
|
|
63
|
+
checked,
|
|
64
|
+
filter: '',
|
|
65
|
+
done: false,
|
|
66
|
+
cancelled: false,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** Moves within the filtered view, skipping disabled entries. Wraps. */
|
|
70
|
+
function move(choices, view, from, delta) {
|
|
71
|
+
const n = view.length;
|
|
72
|
+
if (n === 0)
|
|
73
|
+
return 0;
|
|
74
|
+
let i = from;
|
|
75
|
+
for (let step = 0; step < n; step++) {
|
|
76
|
+
i = (i + delta + n) % n;
|
|
77
|
+
if (!choices[view[i]].disabled)
|
|
78
|
+
return i;
|
|
79
|
+
}
|
|
80
|
+
return from;
|
|
81
|
+
}
|
|
82
|
+
function withFilter(state, choices, filter) {
|
|
83
|
+
const view = visibleIndices(choices, filter);
|
|
84
|
+
return { ...state, filter, cursor: view.length === 0 ? 0 : Math.min(state.cursor, view.length - 1) };
|
|
85
|
+
}
|
|
86
|
+
export function reduce(state, key, choices, multi) {
|
|
87
|
+
const view = visibleIndices(choices, state.filter);
|
|
88
|
+
const under = view[state.cursor];
|
|
89
|
+
switch (key.kind) {
|
|
90
|
+
case 'up':
|
|
91
|
+
return { ...state, cursor: move(choices, view, state.cursor, -1) };
|
|
92
|
+
case 'down':
|
|
93
|
+
return { ...state, cursor: move(choices, view, state.cursor, +1) };
|
|
94
|
+
case 'space': {
|
|
95
|
+
if (!multi || under === undefined || choices[under].disabled)
|
|
96
|
+
return state;
|
|
97
|
+
const checked = new Set(state.checked);
|
|
98
|
+
if (checked.has(under))
|
|
99
|
+
checked.delete(under);
|
|
100
|
+
else
|
|
101
|
+
checked.add(under);
|
|
102
|
+
return { ...state, checked };
|
|
103
|
+
}
|
|
104
|
+
case 'enter': {
|
|
105
|
+
if (multi)
|
|
106
|
+
return { ...state, done: true };
|
|
107
|
+
if (under === undefined || choices[under].disabled)
|
|
108
|
+
return state;
|
|
109
|
+
return { ...state, checked: new Set([under]), done: true };
|
|
110
|
+
}
|
|
111
|
+
case 'char':
|
|
112
|
+
return multi ? withFilter(state, choices, state.filter + key.value) : state;
|
|
113
|
+
case 'backspace':
|
|
114
|
+
return multi ? withFilter(state, choices, state.filter.slice(0, -1)) : state;
|
|
115
|
+
case 'toggleAll': {
|
|
116
|
+
if (!multi)
|
|
117
|
+
return state;
|
|
118
|
+
const enabled = view.filter((i) => !choices[i].disabled);
|
|
119
|
+
const allChecked = enabled.every((i) => state.checked.has(i));
|
|
120
|
+
const checked = new Set(state.checked);
|
|
121
|
+
for (const i of enabled) {
|
|
122
|
+
if (allChecked)
|
|
123
|
+
checked.delete(i);
|
|
124
|
+
else
|
|
125
|
+
checked.add(i);
|
|
126
|
+
}
|
|
127
|
+
return { ...state, checked };
|
|
128
|
+
}
|
|
129
|
+
case 'cancel':
|
|
130
|
+
return { ...state, cancelled: true, done: true };
|
|
131
|
+
case 'back':
|
|
132
|
+
return { ...state, back: true, done: true };
|
|
133
|
+
default:
|
|
134
|
+
return state;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The slice of a list to draw, centred on the cursor.
|
|
139
|
+
*
|
|
140
|
+
* Drawing every choice is not an option: the redraw moves the terminal cursor
|
|
141
|
+
* up by the block height, so a block taller than the screen corrupts it, and
|
|
142
|
+
* one provider alone offers 341 models.
|
|
143
|
+
*/
|
|
144
|
+
export function viewport(cursor, total, height) {
|
|
145
|
+
if (total === 0)
|
|
146
|
+
return { start: 0, end: 0, above: 0, below: 0 };
|
|
147
|
+
const h = Math.min(Math.max(3, height), total);
|
|
148
|
+
let start = cursor - Math.floor(h / 2);
|
|
149
|
+
if (start < 0)
|
|
150
|
+
start = 0;
|
|
151
|
+
if (start + h > total)
|
|
152
|
+
start = total - h;
|
|
153
|
+
const end = start + h;
|
|
154
|
+
return { start, end, above: start, below: total - end };
|
|
155
|
+
}
|
|
156
|
+
/** Rows available for choices, after the title, filter, counts and hint. */
|
|
157
|
+
export function listHeight(rows = process.stdout.rows ?? 24) {
|
|
158
|
+
return Math.max(3, Math.min(15, rows - 8));
|
|
159
|
+
}
|
|
160
|
+
export function renderList(title, choices, state, multi, height = 15, back = false) {
|
|
161
|
+
const view = visibleIndices(choices, state.filter);
|
|
162
|
+
const win = viewport(state.cursor, view.length, height);
|
|
163
|
+
const lines = [` ${title}`, ''];
|
|
164
|
+
if (multi)
|
|
165
|
+
lines.push(` filter: ${state.filter}█`, '');
|
|
166
|
+
if (win.above > 0)
|
|
167
|
+
lines.push(` ↑ ${win.above} more`);
|
|
168
|
+
for (let i = win.start; i < win.end; i++) {
|
|
169
|
+
const orig = view[i];
|
|
170
|
+
const choice = choices[orig];
|
|
171
|
+
const pointer = i === state.cursor ? '❯' : ' ';
|
|
172
|
+
const mark = multi ? (state.checked.has(orig) ? '◉' : '○') : '';
|
|
173
|
+
const hint = choice.hint ? ` · ${choice.hint}` : '';
|
|
174
|
+
const label = choice.disabled ? `${choice.label} (unavailable)` : choice.label;
|
|
175
|
+
lines.push(` ${pointer} ${mark} ${label}${hint}`.replace(/\s+$/, ''));
|
|
176
|
+
}
|
|
177
|
+
if (win.below > 0)
|
|
178
|
+
lines.push(` ↓ ${win.below} more`);
|
|
179
|
+
lines.push('');
|
|
180
|
+
lines.push(multi
|
|
181
|
+
? ` ${view.length} of ${choices.length} · space toggle · type to filter · enter confirm${back ? ' · ← back' : ''} · esc cancel`
|
|
182
|
+
: ` ↑↓ move · enter select${back ? ' · ← back' : ''} · esc cancel`);
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Builds one redraw of a block, and reports the height to pass back next time.
|
|
187
|
+
*
|
|
188
|
+
* Uses clear-screen rather than per-line cursor-up: some terminal
|
|
189
|
+
* environments (cmux panels, certain multiplexer panes) do not process
|
|
190
|
+
* CSI cursor-movement sequences, which causes cursor-up redraws to
|
|
191
|
+
* append the block rather than overwrite it. Clear-screen is universally
|
|
192
|
+
* handled.
|
|
193
|
+
*/
|
|
194
|
+
export function redraw(body, lastHeight) {
|
|
195
|
+
const lines = body.split('\n');
|
|
196
|
+
const clear = '\u001b[2J\u001b[H';
|
|
197
|
+
return {
|
|
198
|
+
out: `${clear}${lines.join('\n')}\n`,
|
|
199
|
+
height: lines.length,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* The wordmark, for commands a person reads rather than pipes.
|
|
204
|
+
*
|
|
205
|
+
* Deliberately plain text: no colour, so it survives a pale terminal, a dark
|
|
206
|
+
* one, and a pipe into a file equally. Widest row is 54 columns, which leaves
|
|
207
|
+
* room on an 80-column terminal.
|
|
208
|
+
*/
|
|
209
|
+
export function banner() {
|
|
210
|
+
return [
|
|
211
|
+
' ███████╗ ██████╗ ███╗ ██╗ █████╗ ████████╗ █████╗',
|
|
212
|
+
' ██╔════╝██╔═══██╗████╗ ██║██╔══██╗╚══██╔══╝██╔══██╗',
|
|
213
|
+
' ███████╗██║ ██║██╔██╗ ██║███████║ ██║ ███████║',
|
|
214
|
+
' ╚════██║██║ ██║██║╚██╗██║██╔══██║ ██║ ██╔══██║',
|
|
215
|
+
' ███████║╚██████╔╝██║ ╚████║██║ ██║ ██║ ██║ ██║',
|
|
216
|
+
' ╚══════╝ ╚═════╝ ╚═╝ ╚═══╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝',
|
|
217
|
+
'',
|
|
218
|
+
' foreign-model subagents for Claude Code',
|
|
219
|
+
].join('\n');
|
|
220
|
+
}
|
|
221
|
+
export function isInteractive() {
|
|
222
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
223
|
+
}
|
|
224
|
+
export class CancelledError extends Error {
|
|
225
|
+
constructor() {
|
|
226
|
+
super('cancelled');
|
|
227
|
+
this.name = 'CancelledError';
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Left was pressed on a screen that offers a previous step.
|
|
232
|
+
*
|
|
233
|
+
* A thrown sentinel rather than a return value, so every prompt keeps its
|
|
234
|
+
* ordinary return type and only the wizard that knows the step order has to
|
|
235
|
+
* handle going back.
|
|
236
|
+
*/
|
|
237
|
+
export class BackError extends Error {
|
|
238
|
+
constructor() {
|
|
239
|
+
super('back');
|
|
240
|
+
this.name = 'BackError';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Puts stdin in raw mode, feeds every keystroke to `step`, and restores it.
|
|
245
|
+
*
|
|
246
|
+
* Owning the whole lifecycle here is deliberate: stdin is shared by every
|
|
247
|
+
* prompt in a run, and two details have to be right or the *next* prompt
|
|
248
|
+
* breaks rather than this one.
|
|
249
|
+
*
|
|
250
|
+
* - `destroyOnReturn: false` — the default async iterator destroys the
|
|
251
|
+
* stream when the loop ends, so a later prompt would reject with "The
|
|
252
|
+
* operation was aborted".
|
|
253
|
+
* - no `resume()` — that switches stdin to flowing mode, where chunks are
|
|
254
|
+
* dropped in the gap before the next prompt's iterator attaches. The
|
|
255
|
+
* iterator pulls on its own; it needs no help.
|
|
256
|
+
* - `ref()` — a paused stdin's handle is *unreferenced*, so it does not keep
|
|
257
|
+
* the event loop alive. Waiting on a keystroke is then not work Node knows
|
|
258
|
+
* about: with nothing else pending it exits, code 0, mid-prompt. Nothing
|
|
259
|
+
* paused stdin before the Ink wizard existed; Ink pauses it on unmount, so
|
|
260
|
+
* every prompt after the wizard died the moment it was drawn — the prompt
|
|
261
|
+
* on screen, the shell back, and no config written. That was
|
|
262
|
+
* "sonata init never saves".
|
|
263
|
+
*/
|
|
264
|
+
export async function readKeys(stdin, step) {
|
|
265
|
+
stdin.setRawMode?.(true);
|
|
266
|
+
stdin.setEncoding('utf8');
|
|
267
|
+
stdin.ref?.();
|
|
268
|
+
try {
|
|
269
|
+
for await (const chunk of stdin.iterator({ destroyOnReturn: false })) {
|
|
270
|
+
if (step(String(chunk)))
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
stdin.setRawMode?.(false);
|
|
276
|
+
stdin.pause();
|
|
277
|
+
// Released again, or the process would hang after the last prompt instead
|
|
278
|
+
// of exiting — the same refcount, in the opposite direction.
|
|
279
|
+
stdin.unref?.();
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
async function runList(title, choices, multi, back = false) {
|
|
283
|
+
if (!isInteractive()) {
|
|
284
|
+
throw new Error('sonata: this command needs an interactive terminal. ' +
|
|
285
|
+
'Use the non-interactive flags instead (see `sonata init --help`).');
|
|
286
|
+
}
|
|
287
|
+
if (choices.length === 0)
|
|
288
|
+
return [];
|
|
289
|
+
let state = initialState(choices);
|
|
290
|
+
const stdin = process.stdin;
|
|
291
|
+
const stdout = process.stdout;
|
|
292
|
+
const height = listHeight();
|
|
293
|
+
stdout.write('\u001b[?1049h');
|
|
294
|
+
let lastHeight = 0;
|
|
295
|
+
const draw = () => {
|
|
296
|
+
const { out, height: drawn } = redraw(renderList(title, choices, state, multi, height, back), lastHeight);
|
|
297
|
+
stdout.write(out);
|
|
298
|
+
lastHeight = drawn;
|
|
299
|
+
};
|
|
300
|
+
try {
|
|
301
|
+
draw();
|
|
302
|
+
await readKeys(stdin, (chunk) => {
|
|
303
|
+
state = reduce(state, parseKey(chunk, multi), choices, multi);
|
|
304
|
+
if (state.done)
|
|
305
|
+
return true;
|
|
306
|
+
draw();
|
|
307
|
+
return false;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
stdout.write('\u001b[?1049l');
|
|
312
|
+
}
|
|
313
|
+
if (state.cancelled)
|
|
314
|
+
throw new CancelledError();
|
|
315
|
+
// Only honoured where the caller said a previous step exists; otherwise Left
|
|
316
|
+
// is inert rather than an error the wizard would have to swallow.
|
|
317
|
+
if (state.back && back)
|
|
318
|
+
throw new BackError();
|
|
319
|
+
return [...state.checked].sort((a, b) => a - b).map((i) => choices[i].value);
|
|
320
|
+
}
|
|
321
|
+
export async function select(title, choices, back = false) {
|
|
322
|
+
const picked = await runList(title, choices, false, back);
|
|
323
|
+
return picked[0];
|
|
324
|
+
}
|
|
325
|
+
export async function confirm(question, defaultYes, back = false) {
|
|
326
|
+
const yes = { value: true, label: 'Yes', checked: defaultYes };
|
|
327
|
+
const no = { value: false, label: 'No', checked: !defaultYes };
|
|
328
|
+
return select(question, defaultYes ? [yes, no] : [no, yes], back);
|
|
329
|
+
}
|
|
330
|
+
//# sourceMappingURL=tui.js.map
|
package/dist/tui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/watchdog.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
/**
|
|
3
|
+
* Wraps a harness script with a watchdog that kills it if it overruns.
|
|
4
|
+
* Enforcement lives in the launched shell, not in `sonata tail`, so a run
|
|
5
|
+
* with nobody watching is still bounded.
|
|
6
|
+
*/
|
|
7
|
+
export function wrapWithTimeout(input) {
|
|
8
|
+
const harnessLog = join(input.runDir, 'harness.log');
|
|
9
|
+
const timeoutMark = join(input.runDir, 'timeout');
|
|
10
|
+
const exitPath = join(input.runDir, 'exit');
|
|
11
|
+
return [
|
|
12
|
+
'#!/bin/bash',
|
|
13
|
+
'# Job control gives the background harness its own process group, so the',
|
|
14
|
+
'# watchdog can kill the whole tree. A harness is typically a shell running',
|
|
15
|
+
'# a Node CLI that spawns further children; killing only direct children',
|
|
16
|
+
'# leaves those grandchildren running.',
|
|
17
|
+
'set -m',
|
|
18
|
+
`bash '${input.harnessScriptPath}' &`,
|
|
19
|
+
'HARNESS_PID=$!',
|
|
20
|
+
'',
|
|
21
|
+
'(',
|
|
22
|
+
` sleep ${input.timeoutSeconds}`,
|
|
23
|
+
' if kill -0 $HARNESS_PID 2>/dev/null; then',
|
|
24
|
+
` echo 'sonata: run timeout after ${input.timeoutSeconds}s' >> '${harnessLog}'`,
|
|
25
|
+
` echo timeout > '${timeoutMark}'`,
|
|
26
|
+
' pkill -P $HARNESS_PID 2>/dev/null',
|
|
27
|
+
' kill -TERM -$HARNESS_PID 2>/dev/null || kill -TERM $HARNESS_PID 2>/dev/null',
|
|
28
|
+
' sleep 5',
|
|
29
|
+
' kill -KILL -$HARNESS_PID 2>/dev/null || kill -KILL $HARNESS_PID 2>/dev/null',
|
|
30
|
+
' fi',
|
|
31
|
+
') &',
|
|
32
|
+
'WATCHDOG_PID=$!',
|
|
33
|
+
'# Job control reports a killed background job to the terminal. On a normal',
|
|
34
|
+
'# run the watchdog is always killed, so without this every completed run',
|
|
35
|
+
'# ended with a "Terminated: 15" line in the pane — which then became the',
|
|
36
|
+
'# tail of any degraded report. Disowning it silences the report only; the',
|
|
37
|
+
'# watchdog still runs, and still kills the tree if it fires.',
|
|
38
|
+
'disown $WATCHDOG_PID 2>/dev/null',
|
|
39
|
+
'',
|
|
40
|
+
'# `set -m` above put the harness in its own process group, which means it is',
|
|
41
|
+
'# NOT the terminal\'s foreground group. Any harness that reads the terminal —',
|
|
42
|
+
'# every interactive TUI, which is how sonata honours `default` mode — then',
|
|
43
|
+
'# takes SIGTTIN and stops dead. Reproduced with a one-line harness that reads',
|
|
44
|
+
'# a single key: the pane freezes and the process sits in state T forever,',
|
|
45
|
+
'# until the run timeout kills something that never started.',
|
|
46
|
+
'#',
|
|
47
|
+
'# `fg` hands the terminal to that process group and waits for it exactly as',
|
|
48
|
+
'# `wait` did, returning the same status.',
|
|
49
|
+
'#',
|
|
50
|
+
'# Its output is deliberately NOT redirected. `fg %1 >/dev/null 2>&1` runs and',
|
|
51
|
+
'# reports success, but the job stays stopped — the handover simply does not',
|
|
52
|
+
'# happen. Reproduced both ways against the same wrapper. The cost is one',
|
|
53
|
+
'# extra pane line, the job command that fg echoes.',
|
|
54
|
+
'#',
|
|
55
|
+
'# With no terminal there is nothing to hand over and no job control to do it',
|
|
56
|
+
'# with, so fall back to the plain wait.',
|
|
57
|
+
...(input.interactive !== false
|
|
58
|
+
? [
|
|
59
|
+
'if [ -t 0 ]; then',
|
|
60
|
+
' fg %1',
|
|
61
|
+
' STATUS=$?',
|
|
62
|
+
'else',
|
|
63
|
+
' wait $HARNESS_PID',
|
|
64
|
+
' STATUS=$?',
|
|
65
|
+
'fi',
|
|
66
|
+
]
|
|
67
|
+
: [
|
|
68
|
+
'# Non-interactive harness: skip fg, use wait. fg interferes with',
|
|
69
|
+
'# output redirection in some harnesses (claude -p hangs under fg).',
|
|
70
|
+
'wait $HARNESS_PID',
|
|
71
|
+
'STATUS=$?',
|
|
72
|
+
]),
|
|
73
|
+
'',
|
|
74
|
+
'kill $WATCHDOG_PID 2>/dev/null',
|
|
75
|
+
'pkill -P $WATCHDOG_PID 2>/dev/null',
|
|
76
|
+
'wait $WATCHDOG_PID 2>/dev/null',
|
|
77
|
+
'',
|
|
78
|
+
`if [ ! -f '${exitPath}' ]; then`,
|
|
79
|
+
` echo $STATUS > '${exitPath}'`,
|
|
80
|
+
'fi',
|
|
81
|
+
'',
|
|
82
|
+
'exit $STATUS',
|
|
83
|
+
].join('\n') + '\n';
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=watchdog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watchdog.js","sourceRoot":"","sources":["../src/watchdog.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAajC;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,KAAoB;IAClD,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACrD,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAE5C,OAAO;QACL,aAAa;QACb,0EAA0E;QAC1E,4EAA4E;QAC5E,yEAAyE;QACzE,uCAAuC;QACvC,QAAQ;QACR,SAAS,KAAK,CAAC,iBAAiB,KAAK;QACrC,gBAAgB;QAChB,EAAE;QACF,GAAG;QACH,WAAW,KAAK,CAAC,cAAc,EAAE;QACjC,6CAA6C;QAC7C,uCAAuC,KAAK,CAAC,cAAc,UAAU,UAAU,GAAG;QAClF,uBAAuB,WAAW,GAAG;QACrC,uCAAuC;QACvC,iFAAiF;QACjF,aAAa;QACb,iFAAiF;QACjF,MAAM;QACN,KAAK;QACL,iBAAiB;QACjB,4EAA4E;QAC5E,0EAA0E;QAC1E,0EAA0E;QAC1E,2EAA2E;QAC3E,8DAA8D;QAC9D,kCAAkC;QAClC,EAAE;QACF,8EAA8E;QAC9E,+EAA+E;QAC/E,4EAA4E;QAC5E,+EAA+E;QAC/E,2EAA2E;QAC3E,6DAA6D;QAC7D,GAAG;QACH,6EAA6E;QAC7E,0CAA0C;QAC1C,GAAG;QACH,+EAA+E;QAC/E,6EAA6E;QAC7E,0EAA0E;QAC1E,oDAAoD;QACpD,GAAG;QACH,8EAA8E;QAC9E,yCAAyC;QACzC,GAAG,CAAC,KAAK,CAAC,WAAW,KAAK,KAAK;YAC7B,CAAC,CAAC;gBACA,mBAAmB;gBACnB,SAAS;gBACT,aAAa;gBACb,MAAM;gBACN,qBAAqB;gBACrB,aAAa;gBACb,IAAI;aACL;YACD,CAAC,CAAC;gBACA,kEAAkE;gBAClE,oEAAoE;gBACpE,mBAAmB;gBACnB,WAAW;aACZ,CAAC;QACJ,EAAE;QACF,gCAAgC;QAChC,oCAAoC;QACpC,gCAAgC;QAChC,EAAE;QACF,cAAc,QAAQ,WAAW;QACjC,qBAAqB,QAAQ,GAAG;QAChC,IAAI;QACJ,EAAE;QACF,cAAc;KACf,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook: records Claude Code's live permission mode so sonata can
|
|
3
|
+
// mirror it onto a foreign harness. The mode is not available as an env var.
|
|
4
|
+
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
|
|
5
|
+
import { homedir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Where this session's mode belongs, or null when sonata is not in play.
|
|
10
|
+
*
|
|
11
|
+
* Mirrors how sonata resolves its config: a project that has its own setup
|
|
12
|
+
* keeps its mode beside it, and a project relying on the machine config keeps
|
|
13
|
+
* its mode beside *that*.
|
|
14
|
+
*
|
|
15
|
+
* The second case is why this is not simply "does the repo use sonata". With a
|
|
16
|
+
* machine config, every directory is potentially a sonata project — but the
|
|
17
|
+
* hook is installed globally and fires on every Bash call, so writing into the
|
|
18
|
+
* repo would scatter .sonata/ directories across the whole machine. Writing
|
|
19
|
+
* beside the machine config instead keeps one location for the sessions that
|
|
20
|
+
* have no project of their own.
|
|
21
|
+
*/
|
|
22
|
+
function modeDir(cwd, home) {
|
|
23
|
+
if (existsSync(join(cwd, 'sonata.toml')) || existsSync(join(cwd, '.sonata'))) {
|
|
24
|
+
return join(cwd, '.sonata');
|
|
25
|
+
}
|
|
26
|
+
const machine = join(home, '.config', 'sonata');
|
|
27
|
+
if (existsSync(join(machine, 'sonata.toml'))) return machine;
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let input = '';
|
|
32
|
+
process.stdin.setEncoding('utf8');
|
|
33
|
+
process.stdin.on('data', (c) => { input += c; });
|
|
34
|
+
process.stdin.on('end', () => {
|
|
35
|
+
try {
|
|
36
|
+
const payload = JSON.parse(input || '{}');
|
|
37
|
+
const sessionId = payload.session_id ?? payload.sessionId;
|
|
38
|
+
const mode = payload.permission_mode ?? payload.permissionMode ?? 'default';
|
|
39
|
+
if (!sessionId) process.exit(0);
|
|
40
|
+
|
|
41
|
+
const cwd = process.env.SONATA_CWD ?? payload.cwd ?? process.cwd();
|
|
42
|
+
const home = process.env.SONATA_HOME ?? homedir();
|
|
43
|
+
|
|
44
|
+
const dir = modeDir(cwd, home);
|
|
45
|
+
if (dir === null) process.exit(0);
|
|
46
|
+
|
|
47
|
+
mkdirSync(dir, { recursive: true });
|
|
48
|
+
writeFileSync(
|
|
49
|
+
join(dir, `session-${sessionId}.json`),
|
|
50
|
+
JSON.stringify({ permissionMode: mode, updatedAt: new Date().toISOString() }),
|
|
51
|
+
);
|
|
52
|
+
} catch {
|
|
53
|
+
// A hook must never break the session it observes.
|
|
54
|
+
}
|
|
55
|
+
process.exit(0);
|
|
56
|
+
});
|