nfo-cli 0.0.2-c → 0.0.3-a
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/README.md +19 -8
- package/dist/cli.js +64 -54
- package/dist/cli.js.map +1 -1
- package/dist/commands/restore.js +0 -1
- package/dist/commands/restore.js.map +1 -1
- package/dist/commands/tui.js +6 -4
- package/dist/commands/tui.js.map +1 -1
- package/dist/permission.js +8 -8
- package/dist/permission.js.map +1 -1
- package/dist/prompts/orchestrator-role.js +6 -7
- package/dist/prompts/orchestrator-role.js.map +1 -1
- package/dist/tui/App.js +5 -5
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/AppView.js +1 -1
- package/dist/tui/AppView.js.map +1 -1
- package/dist/tui/SidebarHeader.js +1 -1
- package/dist/tui/SidebarHeader.js.map +1 -1
- package/dist/tui/StatusBar.js +1 -1
- package/dist/tui/StatusBar.js.map +1 -1
- package/dist/tui/components/App.js +428 -0
- package/dist/tui/components/App.js.map +1 -0
- package/dist/tui/components/AppView.js +13 -0
- package/dist/tui/components/AppView.js.map +1 -0
- package/dist/tui/components/Auditorium.js +17 -0
- package/dist/tui/components/Auditorium.js.map +1 -0
- package/dist/tui/components/ConcertHall.js +11 -0
- package/dist/tui/components/ConcertHall.js.map +1 -0
- package/dist/tui/components/Help.js +41 -0
- package/dist/tui/components/Help.js.map +1 -0
- package/dist/tui/components/OrchestratorPane.js +34 -0
- package/dist/tui/components/OrchestratorPane.js.map +1 -0
- package/dist/tui/components/SidebarHeader.js +6 -0
- package/dist/tui/components/SidebarHeader.js.map +1 -0
- package/dist/tui/components/StatusBar.js +6 -0
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/package.json +2 -2
- package/src/cli.ts +119 -86
- package/src/commands/tui.tsx +10 -4
- package/src/permission.ts +8 -8
- package/src/prompts/orchestrator-role.ts +3 -2
- package/src/tui/{App.tsx → components/App.tsx} +22 -20
- package/src/tui/{AppView.tsx → components/AppView.tsx} +5 -3
- package/src/tui/{Auditorium.tsx → components/Auditorium.tsx} +3 -3
- package/src/tui/{ConcertHall.tsx → components/ConcertHall.tsx} +1 -1
- package/src/tui/{Help.tsx → components/Help.tsx} +0 -9
- package/src/tui/{OrchestratorPane.tsx → components/OrchestratorPane.tsx} +1 -1
- package/src/tui/{SidebarHeader.tsx → components/SidebarHeader.tsx} +3 -1
- package/src/tui/{StatusBar.tsx → components/StatusBar.tsx} +1 -3
- package/tests/permission.test.ts +15 -5
- package/tests/tui/AppView.test.tsx +2 -2
- package/tests/tui/Auditorium.test.tsx +1 -1
- package/tests/tui/ConcertHall.test.tsx +1 -1
- package/tests/tui/Help.test.tsx +1 -1
- package/tests/tui/OrchestratorPane.test.ts +1 -1
- package/tests/tui/SidebarHeader.test.tsx +1 -1
- package/tests/tui/StatusBar.test.tsx +1 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
export function StatusBar(props) {
|
|
4
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderTop: true, paddingX: 1, children: [props.pendingCount > 0 ? (_jsxs(Text, { color: "yellow", children: ["\u26A0 ", props.pendingCount, " awaiting permission \u00B7 [p] jump to next"] })) : null, props.dismissConfirmation ? (_jsx(Text, { color: "red", children: props.dismissConfirmation })) : null, _jsxs(Text, { children: [props.permissionLevel, " \u00B7 ", props.tokenHint] }), props.orchestratorFocused ? (_jsx(Text, { dimColor: true, children: "[type] active terminal [Ctrl+g] sidebar" })) : (_jsx(Text, { dimColor: true, children: "[\u2191\u2193] nav [\u23CE] open left pane [d] dismiss [p] pending [n] notes [Ctrl+g] terminal" })), _jsx(Text, { dimColor: true, children: "[q] detach [?] help" })] }));
|
|
5
|
+
}
|
|
6
|
+
//# sourceMappingURL=StatusBar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"StatusBar.js","sourceRoot":"","sources":["../../../src/tui/components/StatusBar.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAUhC,MAAM,UAAU,SAAS,CAAC,KAAqB;IAC7C,OAAO,CACL,MAAC,GAAG,IACF,aAAa,EAAC,QAAQ,EACtB,WAAW,EAAC,QAAQ,EACpB,SAAS,EAAE,IAAI,EACf,QAAQ,EAAE,CAAC,aAEV,KAAK,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,CAAC,CACxB,MAAC,IAAI,IAAC,KAAK,EAAC,QAAQ,wBACf,KAAK,CAAC,YAAY,oDAChB,CACR,CAAC,CAAC,CAAC,IAAI,EACP,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAC3B,KAAC,IAAI,IAAC,KAAK,EAAC,KAAK,YAAE,KAAK,CAAC,mBAAmB,GAAQ,CACrD,CAAC,CAAC,CAAC,IAAI,EACR,MAAC,IAAI,eACF,KAAK,CAAC,eAAe,cAAK,KAAK,CAAC,SAAS,IACrC,EACN,KAAK,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAC3B,KAAC,IAAI,IAAC,QAAQ,EAAE,IAAI,wDAAgD,CACrE,CAAC,CAAC,CAAC,CACF,KAAC,IAAI,IAAC,QAAQ,EAAE,IAAI,+GAGb,CACR,EACD,KAAC,IAAI,IAAC,QAAQ,EAAE,IAAI,oCAA4B,IAC5C,CACP,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nfo-cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3-a",
|
|
4
|
+
"description": "No Fluff Orchestra — a simple, fluff-free CLI multi-agent orchestrator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nfo": "./dist/cli.js"
|
package/src/cli.ts
CHANGED
|
@@ -1,99 +1,117 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { Command } from
|
|
3
|
-
import { decideAction, createOrchestra } from
|
|
4
|
-
import { attachOrRestore } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { decideAction, createOrchestra } from "./commands/launch.js";
|
|
4
|
+
import { attachOrRestore } from "./commands/attach.js";
|
|
5
|
+
import {
|
|
6
|
+
listOrchestras,
|
|
7
|
+
formatOrchestraList,
|
|
8
|
+
type OrchestraSummary,
|
|
9
|
+
} from "./commands/list.js";
|
|
10
|
+
import {
|
|
11
|
+
isPermissionLevel,
|
|
12
|
+
DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE,
|
|
13
|
+
DANGEROUSLY_SKIP_PERMISSIONS_WARNING,
|
|
14
|
+
type PermissionLevel,
|
|
15
|
+
} from "./permission.js";
|
|
16
|
+
import { detectClaude } from "./claude-detect.js";
|
|
17
|
+
import { createInterface } from "node:readline/promises";
|
|
18
|
+
import packageJson from "../package.json" with { type: "json" };
|
|
9
19
|
|
|
10
20
|
const program = new Command();
|
|
11
21
|
program
|
|
12
|
-
.name(
|
|
13
|
-
.description(
|
|
14
|
-
.version(
|
|
22
|
+
.name("nfo")
|
|
23
|
+
.description("NoFluffOrchestra — TUI multi-agent orchestrator")
|
|
24
|
+
.version(packageJson.version);
|
|
15
25
|
|
|
16
26
|
program
|
|
17
|
-
.argument(
|
|
18
|
-
.option(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
case 'create': {
|
|
29
|
-
const level = await promptPermissionLevel();
|
|
30
|
-
await createOrchestra({
|
|
31
|
-
repoRoot: decision.repoRoot,
|
|
32
|
-
orchestraId: decision.orchestraId,
|
|
33
|
-
permissionLevel: level,
|
|
34
|
-
notifyOnPermission: opts.notifyOnPermission,
|
|
35
|
-
});
|
|
27
|
+
.argument("[id]", "Orchestra id to attach (optional)")
|
|
28
|
+
.option(
|
|
29
|
+
"--notify-on-permission",
|
|
30
|
+
"bell + desktop notify when a musician awaits permission",
|
|
31
|
+
)
|
|
32
|
+
.action(
|
|
33
|
+
async (id: string | undefined, opts: { notifyOnPermission?: boolean }) => {
|
|
34
|
+
await detectClaude();
|
|
35
|
+
try {
|
|
36
|
+
if (id) {
|
|
37
|
+
await attachOrRestore(id);
|
|
36
38
|
return;
|
|
37
39
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const decision = await decideAction(process.cwd());
|
|
41
|
+
switch (decision.kind) {
|
|
42
|
+
case "create": {
|
|
43
|
+
const level = await promptPermissionLevel();
|
|
44
|
+
await createOrchestra({
|
|
45
|
+
repoRoot: decision.repoRoot,
|
|
46
|
+
orchestraId: decision.orchestraId,
|
|
47
|
+
permissionLevel: level,
|
|
48
|
+
notifyOnPermission: opts.notifyOnPermission,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
case "attach_existing":
|
|
53
|
+
await attachOrRestore(decision.orchestraId);
|
|
54
|
+
return;
|
|
55
|
+
case "pick": {
|
|
56
|
+
const picked = await promptOrchestraPicker(decision.summaries);
|
|
57
|
+
await attachOrRestore(picked);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
case "error":
|
|
61
|
+
console.error(decision.message);
|
|
62
|
+
process.exit(1);
|
|
45
63
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
66
|
+
process.exit(1);
|
|
49
67
|
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
process.exit(1);
|
|
53
|
-
}
|
|
54
|
-
});
|
|
68
|
+
},
|
|
69
|
+
);
|
|
55
70
|
|
|
56
71
|
program
|
|
57
|
-
.command(
|
|
58
|
-
.description(
|
|
72
|
+
.command("list")
|
|
73
|
+
.description("List all known orchestras")
|
|
59
74
|
.action(async () => {
|
|
60
75
|
const summaries = await listOrchestras();
|
|
61
76
|
console.log(formatOrchestraList(summaries));
|
|
62
77
|
});
|
|
63
78
|
|
|
64
79
|
program
|
|
65
|
-
.command(
|
|
66
|
-
.description(
|
|
67
|
-
.option(
|
|
80
|
+
.command("restore <id>")
|
|
81
|
+
.description("Force-restore a stopped orchestra")
|
|
82
|
+
.option(
|
|
83
|
+
"--notify-on-permission",
|
|
84
|
+
"bell + desktop notify when a musician awaits permission",
|
|
85
|
+
)
|
|
68
86
|
.action(async (id: string, opts: { notifyOnPermission?: boolean }) => {
|
|
69
|
-
const { restoreOrchestra } = await import(
|
|
87
|
+
const { restoreOrchestra } = await import("./commands/restore.js");
|
|
70
88
|
await restoreOrchestra(id, undefined, opts.notifyOnPermission);
|
|
71
89
|
});
|
|
72
90
|
|
|
73
91
|
program
|
|
74
|
-
.command(
|
|
75
|
-
.description(
|
|
76
|
-
.option(
|
|
92
|
+
.command("kill <id>")
|
|
93
|
+
.description("Tear down an orchestra (state archived, notes preserved)")
|
|
94
|
+
.option("-y, --yes", "Skip confirmation prompt")
|
|
77
95
|
.action(async (id: string, opts: { yes?: boolean }) => {
|
|
78
|
-
const { killOrchestra } = await import(
|
|
96
|
+
const { killOrchestra } = await import("./commands/kill.js");
|
|
79
97
|
await killOrchestra(id, opts);
|
|
80
98
|
});
|
|
81
99
|
|
|
82
100
|
program
|
|
83
|
-
.command(
|
|
84
|
-
.description(
|
|
101
|
+
.command("notes <id>")
|
|
102
|
+
.description("Open the orchestra's notes/ directory in $EDITOR")
|
|
85
103
|
.action(async (id: string) => {
|
|
86
|
-
const { openNotes } = await import(
|
|
104
|
+
const { openNotes } = await import("./commands/notes.js");
|
|
87
105
|
await openNotes(id);
|
|
88
106
|
});
|
|
89
107
|
|
|
90
108
|
program
|
|
91
|
-
.command(
|
|
92
|
-
.description(
|
|
93
|
-
.requiredOption(
|
|
94
|
-
.option(
|
|
109
|
+
.command("mcp-server", { hidden: true })
|
|
110
|
+
.description("(internal) Run the NFO MCP server attached to an orchestra")
|
|
111
|
+
.requiredOption("--orchestra-id <id>", "Orchestra id")
|
|
112
|
+
.option("--caller-musician-id <id>", "When the server is hosting a Musician")
|
|
95
113
|
.action(async (opts: { orchestraId: string; callerMusicianId?: string }) => {
|
|
96
|
-
const { runMcpServerCli } = await import(
|
|
114
|
+
const { runMcpServerCli } = await import("./commands/mcp-server.js");
|
|
97
115
|
await runMcpServerCli({
|
|
98
116
|
orchestraId: opts.orchestraId,
|
|
99
117
|
callerMusicianId: opts.callerMusicianId,
|
|
@@ -101,12 +119,15 @@ program
|
|
|
101
119
|
});
|
|
102
120
|
|
|
103
121
|
program
|
|
104
|
-
.command(
|
|
105
|
-
.description(
|
|
106
|
-
.requiredOption(
|
|
122
|
+
.command("tui", { hidden: true })
|
|
123
|
+
.description("(internal) Run the NFO Ink TUI for an orchestra")
|
|
124
|
+
.requiredOption("--orchestra-id <id>", "Orchestra id")
|
|
107
125
|
.action(async (opts: { orchestraId: string }) => {
|
|
108
|
-
const { runTui } = await import(
|
|
109
|
-
await runTui({
|
|
126
|
+
const { runTui } = await import("./commands/tui.js");
|
|
127
|
+
await runTui({
|
|
128
|
+
orchestraId: opts.orchestraId,
|
|
129
|
+
version: packageJson.version,
|
|
130
|
+
});
|
|
110
131
|
});
|
|
111
132
|
|
|
112
133
|
program.parseAsync(process.argv);
|
|
@@ -114,28 +135,36 @@ program.parseAsync(process.argv);
|
|
|
114
135
|
async function promptPermissionLevel(): Promise<PermissionLevel> {
|
|
115
136
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
116
137
|
try {
|
|
117
|
-
const ans = (
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
const ans = (
|
|
139
|
+
await rl.question(
|
|
140
|
+
`Permission level for this orchestra:
|
|
141
|
+
1) Dangerously skip permissions — RISKY: bypasses all permission checks
|
|
142
|
+
2) auto — Claude auto mode (prompts only on risky actions)
|
|
143
|
+
3) edits — auto-accept edits, prompt on shell/tools
|
|
144
|
+
4) supervised — claude's default prompt-on-risky behavior
|
|
145
|
+
5) strict — read-only / plan mode
|
|
146
|
+
Choose [1-5] (default 4): `,
|
|
147
|
+
)
|
|
148
|
+
).trim();
|
|
125
149
|
|
|
126
150
|
const map: Record<string, PermissionLevel> = {
|
|
127
|
-
|
|
151
|
+
"1": "dangerouslySkipPermissions",
|
|
152
|
+
"2": "auto",
|
|
153
|
+
"3": "acceptEdits",
|
|
154
|
+
"4": "supervised",
|
|
155
|
+
"5": "strict",
|
|
156
|
+
"": "supervised",
|
|
128
157
|
};
|
|
129
158
|
const level = map[ans];
|
|
130
159
|
if (!level || !isPermissionLevel(level)) {
|
|
131
160
|
throw new Error(`Invalid choice: ${ans}`);
|
|
132
161
|
}
|
|
133
162
|
|
|
134
|
-
if (level ===
|
|
135
|
-
console.log(
|
|
136
|
-
const confirm = (await rl.question(
|
|
137
|
-
if (confirm !==
|
|
138
|
-
throw new Error(
|
|
163
|
+
if (level === "dangerouslySkipPermissions") {
|
|
164
|
+
console.log("\n" + DANGEROUSLY_SKIP_PERMISSIONS_WARNING + "\n");
|
|
165
|
+
const confirm = (await rl.question("> ")).trim();
|
|
166
|
+
if (confirm !== DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE) {
|
|
167
|
+
throw new Error("Auto mode not confirmed. Aborting.");
|
|
139
168
|
}
|
|
140
169
|
}
|
|
141
170
|
|
|
@@ -145,17 +174,21 @@ Choose [1-4] (default 3): `,
|
|
|
145
174
|
}
|
|
146
175
|
}
|
|
147
176
|
|
|
148
|
-
async function promptOrchestraPicker(
|
|
177
|
+
async function promptOrchestraPicker(
|
|
178
|
+
summaries: OrchestraSummary[],
|
|
179
|
+
): Promise<string> {
|
|
149
180
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
150
181
|
try {
|
|
151
|
-
console.log(
|
|
182
|
+
console.log("Multiple orchestras found:");
|
|
152
183
|
summaries.forEach((s, i) => {
|
|
153
|
-
console.log(
|
|
184
|
+
console.log(
|
|
185
|
+
` ${i + 1}) ${s.running ? "●" : "○"} ${s.id} (${s.project_path})`,
|
|
186
|
+
);
|
|
154
187
|
});
|
|
155
|
-
const choice = (await rl.question(
|
|
188
|
+
const choice = (await rl.question("Pick one [1-N]: ")).trim();
|
|
156
189
|
const idx = Number(choice) - 1;
|
|
157
190
|
if (Number.isNaN(idx) || idx < 0 || idx >= summaries.length) {
|
|
158
|
-
throw new Error(
|
|
191
|
+
throw new Error("Invalid choice");
|
|
159
192
|
}
|
|
160
193
|
return summaries[idx].id;
|
|
161
194
|
} finally {
|
package/src/commands/tui.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { render } from
|
|
2
|
-
import { App } from
|
|
3
|
-
import { readState } from
|
|
1
|
+
import { render } from "ink";
|
|
2
|
+
import { App } from "../tui/components/App.js";
|
|
3
|
+
import { readState } from "../state.js";
|
|
4
4
|
|
|
5
5
|
export interface RunTuiOptions {
|
|
6
6
|
orchestraId: string;
|
|
7
|
+
version: string;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export async function runTui(opts: RunTuiOptions): Promise<void> {
|
|
@@ -11,6 +12,11 @@ export async function runTui(opts: RunTuiOptions): Promise<void> {
|
|
|
11
12
|
if (!state) {
|
|
12
13
|
throw new Error(`Unknown orchestra: ${opts.orchestraId}`);
|
|
13
14
|
}
|
|
14
|
-
const instance = render(
|
|
15
|
+
const instance = render(
|
|
16
|
+
<App orchestraId={opts.orchestraId} version={opts.version} />,
|
|
17
|
+
{
|
|
18
|
+
exitOnCtrlC: false,
|
|
19
|
+
},
|
|
20
|
+
);
|
|
15
21
|
await instance.waitUntilExit();
|
|
16
22
|
}
|
package/src/permission.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PERMISSION_LEVELS = ['auto', '
|
|
1
|
+
export const PERMISSION_LEVELS = ['dangerouslySkipPermissions', 'auto', 'acceptEdits', 'supervised', 'strict'] as const;
|
|
2
2
|
export type PermissionLevel = (typeof PERMISSION_LEVELS)[number];
|
|
3
3
|
|
|
4
4
|
export function isPermissionLevel(s: string): s is PermissionLevel {
|
|
@@ -7,11 +7,11 @@ export function isPermissionLevel(s: string): s is PermissionLevel {
|
|
|
7
7
|
|
|
8
8
|
export function claudeFlagsForLevel(level: PermissionLevel): string[] {
|
|
9
9
|
switch (level) {
|
|
10
|
-
case '
|
|
11
|
-
// Spec §5.2 + §12.2 open question: exact bypass flag is `--dangerously-skip-permissions`
|
|
12
|
-
// in current Claude Code releases. If a future release renames it, update here.
|
|
10
|
+
case 'dangerouslySkipPermissions':
|
|
13
11
|
return ['--dangerously-skip-permissions'];
|
|
14
|
-
case '
|
|
12
|
+
case 'auto':
|
|
13
|
+
return ['--permission-mode', 'auto'];
|
|
14
|
+
case 'acceptEdits':
|
|
15
15
|
return ['--permission-mode', 'acceptEdits'];
|
|
16
16
|
case 'supervised':
|
|
17
17
|
return ['--permission-mode', 'default'];
|
|
@@ -20,11 +20,11 @@ export function claudeFlagsForLevel(level: PermissionLevel): string[] {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const
|
|
23
|
+
export const DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE = 'I understand';
|
|
24
24
|
|
|
25
|
-
export const
|
|
25
|
+
export const DANGEROUSLY_SKIP_PERMISSIONS_WARNING = `⚠ "Dangerously skip permissions" mode disables all permission checks.
|
|
26
26
|
Musicians can execute arbitrary shell commands, modify files anywhere on
|
|
27
27
|
this system, and access the network without asking. Worktrees limit but
|
|
28
28
|
do not contain risky operations. Use this only in trusted sandboxes or
|
|
29
29
|
when you accept these risks.
|
|
30
|
-
Type "${
|
|
30
|
+
Type "${DANGEROUSLY_SKIP_PERMISSIONS_CONFIRM_PHRASE}" to continue.`;
|
|
@@ -9,8 +9,9 @@ Available NFO tools (in addition to your normal Claude Code tools):
|
|
|
9
9
|
|
|
10
10
|
spawn_musician({ name, task, worktree?, branch_from?, model? })
|
|
11
11
|
Create a Musician with the given task. By default the Musician runs in a
|
|
12
|
-
fresh git worktree off HEAD.
|
|
13
|
-
|
|
12
|
+
fresh git worktree off HEAD. Deploy research musicians that investigate the codebase for the given task. They are only allowed
|
|
13
|
+
to research and report back findings, without modifying the codebase.
|
|
14
|
+
Pass worktree=false for trivially isolated and research work (e.g., docs-only) that doesn't need an isolated branch. Returns the
|
|
14
15
|
musician_id. Provide a model to be used by the Musician, otherwise it defaults to sonnet.
|
|
15
16
|
For trivial tasks Haiku is a good choice; for complex coding work, Sonnet is better.
|
|
16
17
|
|
|
@@ -2,30 +2,30 @@ import type { ReactElement } from "react";
|
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { useInput, useStdout, useWindowSize } from "ink";
|
|
4
4
|
import { AppView } from "./AppView.js";
|
|
5
|
-
import { reduceKey } from "
|
|
6
|
-
import { pollActivity } from "
|
|
5
|
+
import { reduceKey } from "../keymap.js";
|
|
6
|
+
import { pollActivity } from "../poll-activity.js";
|
|
7
7
|
import {
|
|
8
8
|
syncMusicianIdleState,
|
|
9
9
|
type MusicianIdleTracker,
|
|
10
|
-
} from "
|
|
11
|
-
import { pollPermissions } from "
|
|
12
|
-
import { setMusicianStatus } from "
|
|
13
|
-
import { watchOrchestraState, type StopWatching } from "
|
|
14
|
-
import { listOrchestras, type OrchestraSummary } from "
|
|
10
|
+
} from "../poll-idle.js";
|
|
11
|
+
import { pollPermissions } from "../poll-permission.js";
|
|
12
|
+
import { setMusicianStatus } from "../../state-updaters.js";
|
|
13
|
+
import { watchOrchestraState, type StopWatching } from "../watch-state.js";
|
|
14
|
+
import { listOrchestras, type OrchestraSummary } from "../../commands/list.js";
|
|
15
15
|
import {
|
|
16
16
|
EmbeddedTerminal,
|
|
17
17
|
type EmbeddedTerminalSnapshot,
|
|
18
|
-
} from "
|
|
18
|
+
} from "../embedded-terminal.js";
|
|
19
19
|
import {
|
|
20
20
|
claimEmbeddedSessionLease,
|
|
21
21
|
embeddedSessionLeaseIsCurrent,
|
|
22
22
|
runEmbeddedSessionOperation,
|
|
23
|
-
} from "
|
|
23
|
+
} from "../embedded-session-lifecycle.js";
|
|
24
24
|
import {
|
|
25
25
|
toTerminalMouseScroll,
|
|
26
26
|
toTerminalInput,
|
|
27
27
|
toTerminalViewportCommand,
|
|
28
|
-
} from "
|
|
28
|
+
} from "../terminal-input.js";
|
|
29
29
|
import {
|
|
30
30
|
detachCurrentClient,
|
|
31
31
|
embeddedSessionName,
|
|
@@ -33,15 +33,16 @@ import {
|
|
|
33
33
|
killSession,
|
|
34
34
|
selectWindow,
|
|
35
35
|
sessionName,
|
|
36
|
-
} from "
|
|
37
|
-
import { openNotes } from "
|
|
38
|
-
import { dismissMusician } from "
|
|
39
|
-
import { readState } from "
|
|
40
|
-
import { notifyAwaitingPermission } from "
|
|
41
|
-
import type { Musician, OrchestraState } from "
|
|
36
|
+
} from "../../tmux.js";
|
|
37
|
+
import { openNotes } from "../../commands/notes.js";
|
|
38
|
+
import { dismissMusician } from "../../musicians/dismiss.js";
|
|
39
|
+
import { readState } from "../../state.js";
|
|
40
|
+
import { notifyAwaitingPermission } from "../../notify.js";
|
|
41
|
+
import type { Musician, OrchestraState } from "../../state.types.js";
|
|
42
42
|
|
|
43
43
|
export interface AppProps {
|
|
44
44
|
orchestraId: string;
|
|
45
|
+
version: string;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
function textLines(...lines: string[]): EmbeddedTerminalSnapshot["lines"] {
|
|
@@ -358,10 +359,10 @@ export function App(props: AppProps): ReactElement {
|
|
|
358
359
|
const mouseScroll = toTerminalMouseScroll(input);
|
|
359
360
|
if (mouseScroll) {
|
|
360
361
|
const insideTerminalViewport =
|
|
361
|
-
mouseScroll.column >= terminalScreenLeft
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
362
|
+
mouseScroll.column >= terminalScreenLeft &&
|
|
363
|
+
mouseScroll.column <= terminalScreenRight &&
|
|
364
|
+
mouseScroll.row >= terminalScreenTop &&
|
|
365
|
+
mouseScroll.row <= terminalScreenBottom;
|
|
365
366
|
if (insideTerminalViewport) {
|
|
366
367
|
const translatedColumn = mouseScroll.column - terminalScreenLeft + 1;
|
|
367
368
|
const translatedRow = mouseScroll.row - terminalScreenTop + 1;
|
|
@@ -527,6 +528,7 @@ export function App(props: AppProps): ReactElement {
|
|
|
527
528
|
orchestratorConnected={orchestratorSnapshot.connected}
|
|
528
529
|
activeMusicianId={activePaneMusician?.id ?? null}
|
|
529
530
|
orchestratorActive={activePaneMusician === null}
|
|
531
|
+
version={props.version}
|
|
530
532
|
/>
|
|
531
533
|
);
|
|
532
534
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import { Box } from "ink";
|
|
3
|
-
import type { Musician } from "
|
|
4
|
-
import type { OrchestraSummary } from "
|
|
5
|
-
import type { EmbeddedTerminalLine } from "
|
|
3
|
+
import type { Musician } from "../../state.types.js";
|
|
4
|
+
import type { OrchestraSummary } from "../../commands/list.js";
|
|
5
|
+
import type { EmbeddedTerminalLine } from "../embedded-terminal.js";
|
|
6
6
|
import { ConcertHall } from "./ConcertHall.js";
|
|
7
7
|
import { Auditorium } from "./Auditorium.js";
|
|
8
8
|
import { StatusBar } from "./StatusBar.js";
|
|
@@ -28,6 +28,7 @@ export interface AppViewProps {
|
|
|
28
28
|
orchestratorConnected: boolean;
|
|
29
29
|
activeMusicianId?: string | null;
|
|
30
30
|
orchestratorActive?: boolean;
|
|
31
|
+
version: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export function AppView(props: AppViewProps): ReactElement {
|
|
@@ -46,6 +47,7 @@ export function AppView(props: AppViewProps): ReactElement {
|
|
|
46
47
|
orchestraId={props.currentId}
|
|
47
48
|
musicianCount={props.musicians.length}
|
|
48
49
|
pendingCount={pendingCount}
|
|
50
|
+
version={props.version}
|
|
49
51
|
/>
|
|
50
52
|
<ConcertHall
|
|
51
53
|
orchestras={props.orchestras}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import type { Musician } from "
|
|
4
|
-
import { statusIcon, statusColor } from "
|
|
5
|
-
import { formatRelativeTime } from "
|
|
3
|
+
import type { Musician } from "../../state.types.js";
|
|
4
|
+
import { statusIcon, statusColor } from "../status-icon.js";
|
|
5
|
+
import { formatRelativeTime } from "../format-time.js";
|
|
6
6
|
|
|
7
7
|
export interface AuditoriumProps {
|
|
8
8
|
musicians: Musician[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactElement } from "react";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import type { OrchestraSummary } from "
|
|
3
|
+
import type { OrchestraSummary } from "../../commands/list.js";
|
|
4
4
|
|
|
5
5
|
export interface ConcertHallProps {
|
|
6
6
|
orchestras: OrchestraSummary[];
|
|
@@ -42,15 +42,6 @@ const ROWS: Row[] = [
|
|
|
42
42
|
label:
|
|
43
43
|
"scroll the left terminal through local scrollback when the pointer is over that pane",
|
|
44
44
|
},
|
|
45
|
-
{
|
|
46
|
-
key: "F6",
|
|
47
|
-
label: "tmux global key: jump to the dashboard window from any NFO window",
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
key: "F7",
|
|
51
|
-
label:
|
|
52
|
-
"tmux global key: jump to the Orchestrator window from any NFO window",
|
|
53
|
-
},
|
|
54
45
|
{ key: "?", label: "toggle this help / close" },
|
|
55
46
|
];
|
|
56
47
|
|
|
@@ -5,17 +5,19 @@ export interface SidebarHeaderProps {
|
|
|
5
5
|
orchestraId: string;
|
|
6
6
|
musicianCount: number;
|
|
7
7
|
pendingCount: number;
|
|
8
|
+
version: string;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function SidebarHeader(props: SidebarHeaderProps): ReactElement {
|
|
11
12
|
return (
|
|
12
13
|
<Box
|
|
13
14
|
flexDirection="column"
|
|
14
|
-
borderStyle="
|
|
15
|
+
borderStyle="round"
|
|
15
16
|
borderBottom={true}
|
|
16
17
|
paddingX={1}
|
|
17
18
|
>
|
|
18
19
|
<Text bold={true}>No Fluff Orchestra · {props.orchestraId}</Text>
|
|
20
|
+
<Text bold={true}>v.{props.version}</Text>
|
|
19
21
|
{props.pendingCount > 0 ? (
|
|
20
22
|
<Text color="yellow">
|
|
21
23
|
{props.musicianCount} musicians · {props.pendingCount} awaiting
|
|
@@ -36,9 +36,7 @@ export function StatusBar(props: StatusBarProps): ReactElement {
|
|
|
36
36
|
terminal
|
|
37
37
|
</Text>
|
|
38
38
|
)}
|
|
39
|
-
<Text dimColor={true}>
|
|
40
|
-
[q] detach [F6] dashboard [F7] orchestrator [?] help
|
|
41
|
-
</Text>
|
|
39
|
+
<Text dimColor={true}>[q] detach [?] help</Text>
|
|
42
40
|
</Box>
|
|
43
41
|
);
|
|
44
42
|
}
|
package/tests/permission.test.ts
CHANGED
|
@@ -7,13 +7,20 @@ import {
|
|
|
7
7
|
} from '../src/permission.js';
|
|
8
8
|
|
|
9
9
|
describe('permission levels', () => {
|
|
10
|
-
it('lists all
|
|
11
|
-
expect(PERMISSION_LEVELS).toEqual([
|
|
10
|
+
it('lists all five levels in order from most to least permissive', () => {
|
|
11
|
+
expect(PERMISSION_LEVELS).toEqual([
|
|
12
|
+
'dangerouslySkipPermissions',
|
|
13
|
+
'auto',
|
|
14
|
+
'acceptEdits',
|
|
15
|
+
'supervised',
|
|
16
|
+
'strict',
|
|
17
|
+
]);
|
|
12
18
|
});
|
|
13
19
|
|
|
14
20
|
it('isPermissionLevel rejects unknown strings', () => {
|
|
21
|
+
expect(isPermissionLevel('dangerouslySkipPermissions')).toBe(true);
|
|
15
22
|
expect(isPermissionLevel('auto')).toBe(true);
|
|
16
|
-
expect(isPermissionLevel('
|
|
23
|
+
expect(isPermissionLevel('acceptEdits')).toBe(true);
|
|
17
24
|
expect(isPermissionLevel('supervised')).toBe(true);
|
|
18
25
|
expect(isPermissionLevel('strict')).toBe(true);
|
|
19
26
|
expect(isPermissionLevel('YOLO')).toBe(false);
|
|
@@ -21,8 +28,11 @@ describe('permission levels', () => {
|
|
|
21
28
|
});
|
|
22
29
|
|
|
23
30
|
it('claudeFlagsForLevel returns the right flag list per level', () => {
|
|
24
|
-
expect(claudeFlagsForLevel('
|
|
25
|
-
|
|
31
|
+
expect(claudeFlagsForLevel('dangerouslySkipPermissions')).toEqual([
|
|
32
|
+
'--dangerously-skip-permissions',
|
|
33
|
+
]);
|
|
34
|
+
expect(claudeFlagsForLevel('auto')).toEqual(['--permission-mode', 'auto']);
|
|
35
|
+
expect(claudeFlagsForLevel('acceptEdits')).toEqual(['--permission-mode', 'acceptEdits']);
|
|
26
36
|
expect(claudeFlagsForLevel('supervised')).toEqual(['--permission-mode', 'default']);
|
|
27
37
|
expect(claudeFlagsForLevel('strict')).toEqual(['--permission-mode', 'plan']);
|
|
28
38
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { render } from 'ink-testing-library';
|
|
3
|
-
import { AppView } from '../../src/tui/AppView.js';
|
|
4
|
-
import { OrchestratorPane } from '../../src/tui/OrchestratorPane.js';
|
|
3
|
+
import { AppView } from '../../src/tui/components/AppView.js';
|
|
4
|
+
import { OrchestratorPane } from '../../src/tui/components/OrchestratorPane.js';
|
|
5
5
|
import type { Musician } from '../../src/state.types.js';
|
|
6
6
|
import type { OrchestraSummary } from '../../src/commands/list.js';
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { render } from 'ink-testing-library';
|
|
3
|
-
import { Auditorium } from '../../src/tui/Auditorium.js';
|
|
3
|
+
import { Auditorium } from '../../src/tui/components/Auditorium.js';
|
|
4
4
|
import type { Musician } from '../../src/state.types.js';
|
|
5
5
|
|
|
6
6
|
function mus(over: Partial<Musician>): Musician {
|