openshain 0.2.0 → 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.
Files changed (46) hide show
  1. package/NOTICE +4 -0
  2. package/dist/bin.js +4 -33
  3. package/dist/commands/init.d.ts +1 -1
  4. package/dist/commands/init.js +7 -6
  5. package/dist/commands/tools.js +1 -2
  6. package/dist/commands/work.d.ts +1 -9
  7. package/dist/commands/work.js +9 -22
  8. package/dist/index.d.ts +2 -2
  9. package/dist/index.js +2 -2
  10. package/dist/labels.d.ts +1 -2
  11. package/dist/labels.js +3 -0
  12. package/dist/preview.d.ts +13 -0
  13. package/dist/preview.js +141 -0
  14. package/dist/report.d.ts +6 -0
  15. package/dist/{commands/run.js → report.js} +6 -38
  16. package/dist/tui/app.js +38 -5
  17. package/dist/tui/banner.d.ts +3 -8
  18. package/dist/tui/banner.js +2 -2
  19. package/dist/tui/controller.d.ts +30 -5
  20. package/dist/tui/controller.js +306 -77
  21. package/dist/tui/index.js +1 -1
  22. package/dist/tui/lines.d.ts +5 -3
  23. package/dist/tui/lines.js +31 -3
  24. package/dist/tui/markdown.d.ts +15 -0
  25. package/dist/tui/markdown.js +199 -0
  26. package/dist/usage.d.ts +1 -1
  27. package/dist/usage.js +1 -1
  28. package/dist/workspace.js +1 -1
  29. package/package.json +9 -7
  30. package/src/bin.ts +4 -38
  31. package/src/commands/init.ts +7 -6
  32. package/src/commands/tools.ts +7 -2
  33. package/src/commands/work.ts +10 -34
  34. package/src/index.ts +1 -10
  35. package/src/labels.ts +4 -2
  36. package/src/preview.ts +157 -0
  37. package/src/{commands/run.ts → report.ts} +6 -59
  38. package/src/tui/app.tsx +75 -13
  39. package/src/tui/banner.ts +4 -10
  40. package/src/tui/controller.ts +338 -77
  41. package/src/tui/index.ts +3 -1
  42. package/src/tui/lines.ts +36 -6
  43. package/src/tui/markdown.ts +214 -0
  44. package/src/usage.ts +1 -2
  45. package/src/workspace.ts +1 -1
  46. package/dist/commands/run.d.ts +0 -22
package/NOTICE ADDED
@@ -0,0 +1,4 @@
1
+ openshain
2
+ Copyright 2026 openshain contributors
3
+
4
+ Licensed under the Apache License, Version 2.0.
package/dist/bin.js CHANGED
@@ -1,14 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { createInterface } from "node:readline/promises";
3
2
  import { parseArgs } from "node:util";
4
3
  import { anthropicProvider, openaiCompatibleProvider } from "@openshain/agent";
5
4
  import { isOpenshainError } from "@openshain/core";
6
5
  import { standardTools } from "@openshain/tools";
7
6
  import { init } from "./commands/init.js";
8
7
  import { mcp } from "./commands/mcp.js";
9
- import { run } from "./commands/run.js";
10
8
  import { toolsList } from "./commands/tools.js";
11
- import { workList, workResume, workShow } from "./commands/work.js";
9
+ import { workList, workShow } from "./commands/work.js";
12
10
  import { plain } from "./format.js";
13
11
  import { errorLabel } from "./labels.js";
14
12
  import { startTui } from "./tui/index.js";
@@ -16,11 +14,9 @@ import { findWorkspace } from "./workspace.js";
16
14
  const USAGE = `使い方:
17
15
  openshain 端末で対話を始める
18
16
  openshain init openshain.yaml のひな型を書く
19
- openshain run "<依頼>" 依頼を Work として進める
20
17
  openshain tools list 使える Tool の一覧
21
18
  openshain work list Work の一覧
22
19
  openshain work show <id> Work の詳細
23
- openshain work resume <id> 途中で止まった Work を続ける
24
20
  openshain mcp MCP Server を stdio で起動する
25
21
 
26
22
  --workspace <dir> 起点のディレクトリ。省略時はカレントディレクトリ
@@ -65,15 +61,6 @@ async function main(argv) {
65
61
  case "init":
66
62
  await init({ workspaceRoot: values.workspace ?? process.cwd(), write });
67
63
  return 0;
68
- case "run": {
69
- const objective = rest.join(" ").trim();
70
- if (!objective) {
71
- write('依頼の文を指定してください。openshain run "今月の経理を進めて" のように。');
72
- return 2;
73
- }
74
- const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
75
- return withTerminal((ask) => run({ workspaceRoot, providers, objective, write, ...(ask && { ask }) }));
76
- }
77
64
  case "mcp": {
78
65
  const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
79
66
  await mcp({ workspaceRoot, providers });
@@ -91,7 +78,7 @@ async function main(argv) {
91
78
  case "work": {
92
79
  const sub = rest[0];
93
80
  const id = rest[1] ?? "";
94
- if (!(sub === "list" || ((sub === "show" || sub === "resume") && id))) {
81
+ if (!(sub === "list" || (sub === "show" && id))) {
95
82
  write(USAGE);
96
83
  return 2;
97
84
  }
@@ -100,11 +87,8 @@ async function main(argv) {
100
87
  await workList({ workspaceRoot, write });
101
88
  return 0;
102
89
  }
103
- if (sub === "show") {
104
- await workShow({ workspaceRoot, id, write });
105
- return 0;
106
- }
107
- return withTerminal((ask) => workResume({ workspaceRoot, providers, id, write, ...(ask && { ask }) }));
90
+ await workShow({ workspaceRoot, id, write });
91
+ return 0;
108
92
  }
109
93
  default:
110
94
  write(`不明なコマンド ${command}`);
@@ -112,19 +96,6 @@ async function main(argv) {
112
96
  return 2;
113
97
  }
114
98
  }
115
- /** Runs `fn` with a way to ask the person when a terminal is there to answer; otherwise without one. */
116
- async function withTerminal(fn) {
117
- // Without a terminal there is no one to answer; the work waits instead.
118
- if (process.stdin.isTTY !== true || process.stdout.isTTY !== true)
119
- return fn();
120
- const rl = createInterface({ input: process.stdin, output: process.stdout });
121
- try {
122
- return await fn((question) => rl.question(`${plain(question)}\n> `));
123
- }
124
- finally {
125
- rl.close();
126
- }
127
- }
128
99
  main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
129
100
  if (isOpenshainError(err)) {
130
101
  const heading = errorLabel(err.code);
@@ -6,7 +6,7 @@ export declare const CONFIG_TEMPLATE: string;
6
6
  /** Registers the runtime as a project MCP server for Claude Code. `openshain` must be on PATH. */
7
7
  export declare const MCP_TEMPLATE: string;
8
8
  /** What an outside agent reads before working in the folder. Codex reads AGENTS.md; Claude Code reads it through CLAUDE.md. */
9
- export declare const AGENTS_TEMPLATE = "# \u3053\u306E\u4F1A\u793E\u30D5\u30A9\u30EB\u30C0\u3067\u50CD\u304F\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u3078\n\n\u3053\u306E\u30D5\u30A9\u30EB\u30C0\u306F openshain \u306E Company Workspace \u3067\u3059\u3002\u3053\u306E\u6307\u793A\u306F\u3001Claude Code \u3084 Codex \u306E\u3088\u3046\u306A\u5916\u90E8\u306E\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u304C MCP \u7D4C\u7531\u3067\u3053\u306E\u30D5\u30A9\u30EB\u30C0\u3092\u6271\u3046\u3068\u304D\u306E\u3082\u306E\u3067\u3059\u3002`openshain run` \u3067 Runtime \u81EA\u8EAB\u304C\u52D5\u304F\u3068\u304D\u306F\u3001Work \u306E\u4F5C\u6210\u3068\u5B8C\u4E86\u3092 Runtime \u304C\u884C\u3046\u306E\u3067\u3001\u4E0B\u306E work_* \u306E\u624B\u9806\u306F\u5F53\u3066\u306F\u307E\u308A\u307E\u305B\u3093\u3002\n\n\u4F1A\u793E\u306E\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u66F8\u304D\u3068\u96C6\u8A08\u306F openshain \u306E MCP tool \u3067\u884C\u3044\u307E\u3059\u3002Claude Code \u3084 Codex \u81EA\u8EAB\u306E Read\u3001Write\u3001Bash \u306F\u4F1A\u793E\u306E\u30D5\u30A1\u30A4\u30EB\u306B\u306F\u4F7F\u3044\u307E\u305B\u3093\u3002Runtime \u3092\u901A\u3089\u306A\u304B\u3063\u305F\u64CD\u4F5C\u306F\u8A18\u9332\u306B\u6B8B\u3089\u306A\u3044\u305F\u3081\u3067\u3059\u3002\n\n- \u4F9D\u983C\u3092\u53D7\u3051\u305F\u3089\u3001\u307E\u305A `work_create` \u306B\u4F9D\u983C\u306E\u6587\u3092\u305D\u306E\u307E\u307E\u6E21\u3057\u3066 Work \u3092\u4F5C\u308B\n- \u30D5\u30A1\u30A4\u30EB\u306F `fs_list`\u3001`fs_search`\u3001`fs_read`\u3001`csv_read`\u3001`markdown_read` \u3067\u898B\u308B\u3002\u5408\u8A08\u3084\u4EF6\u6570\u306F `csv_aggregate` \u306B\u4EFB\u305B\u3001\u81EA\u5206\u3067\u5408\u8A08\u3057\u306A\u3044\n- \u66F8\u304F\u3068\u304D\u306F `fs_write` \u304B `csv_write`\n- \u7D42\u308F\u3063\u305F\u3089 `work_complete` \u306B\u3001\u4F55\u3092\u3057\u305F\u304B\u3068\u3001\u66F8\u3044\u305F\u30D5\u30A1\u30A4\u30EB\u3092\u6E21\u3059\u3002\u7D9A\u3051\u3089\u308C\u306A\u3044\u3068\u304D\u306F `work_fail`\n- `openshain.yaml` \u3068 `work/` \u306F Runtime \u306E\u3082\u306E\u3002\u5909\u66F4\u3057\u306A\u3044\n";
9
+ export declare const AGENTS_TEMPLATE = "# \u3053\u306E\u4F1A\u793E\u30D5\u30A9\u30EB\u30C0\u3067\u50CD\u304F\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u3078\n\n\u3053\u306E\u30D5\u30A9\u30EB\u30C0\u306F openshain \u306E Company Workspace \u3067\u3059\u3002\u3053\u306E\u6307\u793A\u306F\u3001Claude Code \u3084 Codex \u306E\u3088\u3046\u306A\u5916\u90E8\u306E\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u304C MCP \u7D4C\u7531\u3067\u3053\u306E\u30D5\u30A9\u30EB\u30C0\u3092\u6271\u3046\u3068\u304D\u306E\u3082\u306E\u3067\u3059\u3002openshain \u306E\u5BFE\u8A71\u578B CLI \u3082\u540C\u3058\u624B\u9806\u3067 Runtime \u3092\u4F7F\u3044\u307E\u3059\u3002\n\n\u4F1A\u793E\u306E\u30D5\u30A1\u30A4\u30EB\u306E\u8AAD\u307F\u66F8\u304D\u3068\u96C6\u8A08\u306F openshain \u306E MCP tool \u3067\u884C\u3044\u307E\u3059\u3002Claude Code \u3084 Codex \u81EA\u8EAB\u306E Read\u3001Write\u3001Bash \u306F\u4F1A\u793E\u306E\u30D5\u30A1\u30A4\u30EB\u306B\u306F\u4F7F\u3044\u307E\u305B\u3093\u3002Runtime \u3092\u901A\u3089\u306A\u304B\u3063\u305F\u64CD\u4F5C\u306F\u8A18\u9332\u306B\u6B8B\u3089\u306A\u3044\u305F\u3081\u3067\u3059\u3002\n\n- \u4F9D\u983C\u3092\u53D7\u3051\u305F\u3089\u3001\u307E\u305A `work_create` \u306B\u4F9D\u983C\u306E\u6587\u3092\u305D\u306E\u307E\u307E\u6E21\u3057\u3066 Work \u3092\u4F5C\u308B\n- \u30D5\u30A1\u30A4\u30EB\u306F `fs_list`\u3001`fs_search`\u3001`fs_read`\u3001`csv_read`\u3001`markdown_read` \u3067\u898B\u308B\u3002\u5408\u8A08\u3084\u4EF6\u6570\u306F `csv_aggregate` \u306B\u4EFB\u305B\u3001\u81EA\u5206\u3067\u5408\u8A08\u3057\u306A\u3044\n- \u66F8\u304F\u3068\u304D\u306F `fs_write` \u304B `csv_write`\n- \u7D42\u308F\u3063\u305F\u3089 `work_complete` \u306B\u3001\u4F55\u3092\u3057\u305F\u304B\u3068\u3001\u66F8\u3044\u305F\u30D5\u30A1\u30A4\u30EB\u3092\u6E21\u3059\u3002\u7D9A\u3051\u3089\u308C\u306A\u3044\u3068\u304D\u306F `work_fail`\n- `openshain.yaml`\u3001`work/`\u3001`principals/`\u3001`authority/` \u306F Runtime \u306E\u3082\u306E\u3002\u5909\u66F4\u3057\u306A\u3044\n- \u547C\u3073\u51FA\u3057\u306E\u7D50\u679C\u304C `pending` \u306A\u3089\u3001\u4F1A\u793E\u306E\u6A29\u9650\u306E\u898F\u5247\u304C\u305D\u306E\u547C\u3073\u51FA\u3057\u3092\u6B62\u3081\u3066\u3044\u308B\u3002\u627F\u8A8D\u306F\u4F1A\u793E\u306E\u4EBA\u304C openshain \u306E\u753B\u9762\u3067\u6C7A\u3081\u308B\u3002\u81EA\u5206\u3067 `approval_decide` \u3084 `review_decide` \u3092\u547C\u3093\u3067\u901A\u3055\u306A\u3044\u3002\u4EBA\u306B\u4F1D\u3048\u3066\u3001\u6C7A\u307E\u308B\u307E\u3067\u5225\u306E\u4F5C\u696D\u3092\u3059\u308B\n";
10
10
  export declare const CLAUDE_TEMPLATE = "@AGENTS.md\n";
11
11
  export interface InitOptions {
12
12
  workspaceRoot: string;
@@ -20,7 +20,7 @@ profession:
20
20
  id: generic
21
21
  # model への指示。100,000 文字まで
22
22
  instructions: |
23
- あなたはこの会社の事務担当です。依頼された作業を、workspace 内のファイルだけを使って進めてください。
23
+ あなたはこの会社の一般事務の社員エージェントです。依頼された作業を、workspace 内のファイルだけを使って進めてください。
24
24
  model:
25
25
  provider: anthropic # anthropic | openai-compatible
26
26
  model: claude-opus-5
@@ -44,7 +44,7 @@ export const MCP_TEMPLATE = `${JSON.stringify({ mcpServers: { openshain: { comma
44
44
  /** What an outside agent reads before working in the folder. Codex reads AGENTS.md; Claude Code reads it through CLAUDE.md. */
45
45
  export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェントへ
46
46
 
47
- このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP 経由でこのフォルダを扱うときのものです。\`openshain run\` Runtime 自身が動くときは、Work の作成と完了を Runtime が行うので、下の work_* の手順は当てはまりません。
47
+ このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP 経由でこのフォルダを扱うときのものです。openshain の対話型 CLI も同じ手順で Runtime を使います。
48
48
 
49
49
  会社のファイルの読み書きと集計は openshain の MCP tool で行います。Claude Code や Codex 自身の Read、Write、Bash は会社のファイルには使いません。Runtime を通らなかった操作は記録に残らないためです。
50
50
 
@@ -52,7 +52,8 @@ export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェ
52
52
  - ファイルは \`fs_list\`、\`fs_search\`、\`fs_read\`、\`csv_read\`、\`markdown_read\` で見る。合計や件数は \`csv_aggregate\` に任せ、自分で合計しない
53
53
  - 書くときは \`fs_write\` か \`csv_write\`
54
54
  - 終わったら \`work_complete\` に、何をしたかと、書いたファイルを渡す。続けられないときは \`work_fail\`
55
- - \`openshain.yaml\` と \`work/\` は Runtime のもの。変更しない
55
+ - \`openshain.yaml\`、\`work/\`、\`principals/\`、\`authority/\` は Runtime のもの。変更しない
56
+ - 呼び出しの結果が \`pending\` なら、会社の権限の規則がその呼び出しを止めている。承認は会社の人が openshain の画面で決める。自分で \`approval_decide\` や \`review_decide\` を呼んで通さない。人に伝えて、決まるまで別の作業をする
56
57
  `;
57
58
  export const CLAUDE_TEMPLATE = "@AGENTS.md\n";
58
59
  /**
@@ -96,7 +97,7 @@ export async function init({ workspaceRoot, write }) {
96
97
  write(`${path} はすでにあるので変更しません。`);
97
98
  }
98
99
  }
99
- write("company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain run を実行してください。");
100
+ write("company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain を実行してください。");
100
101
  }
101
102
  /**
102
103
  * Adds the openshain server to a .mcp.json that is already there, keeping every other server.
@@ -108,10 +109,10 @@ async function addToMcpConfig(path) {
108
109
  parsed = JSON.parse(await readFile(path, "utf8"));
109
110
  }
110
111
  catch {
111
- return `${path} はすでにあり、JSON として読めないので変更しません。openshain を手で登録してください。`;
112
+ return `${path} はすでにあり、JSON として読めないので変更しません。openshain を手動で登録してください。`;
112
113
  }
113
114
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
114
- return `${path} はすでにあり、形が違うので変更しません。openshain を手で登録してください。`;
115
+ return `${path} はすでにあり、形が違うので変更しません。openshain を手動で登録してください。`;
115
116
  }
116
117
  const config = parsed;
117
118
  const servers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
@@ -1,5 +1,4 @@
1
- import { ASK_USER, RUNTIME_PROVIDER_ID } from "@openshain/agent";
2
- import { createToolRegistry, loadConfig } from "@openshain/core";
1
+ import { ASK_USER, createToolRegistry, loadConfig, RUNTIME_PROVIDER_ID, } from "@openshain/core";
3
2
  /** Every tool the model can call in this workspace, and the ones the allow lists hide. Needs no model provider. */
4
3
  export async function toolsList({ workspaceRoot, providers, write, }) {
5
4
  const config = await loadConfig(workspaceRoot);
@@ -1,5 +1,4 @@
1
- import { type AnyEvent, type RuntimeProviders, type Work } from "@openshain/core";
2
- import { type DriveOptions } from "./run.ts";
1
+ import { type AnyEvent, type Work } from "@openshain/core";
3
2
  export interface WorkListOptions {
4
3
  workspaceRoot: string;
5
4
  write: (line: string) => void;
@@ -14,10 +13,3 @@ export interface WorkShowOptions {
14
13
  /** Everything about one work: state, outcome, what the tools did, the usage, and who acts next. */
15
14
  export declare function workShow({ workspaceRoot, id, write }: WorkShowOptions): Promise<void>;
16
15
  export declare function describeWork(work: Work, events: AnyEvent[]): string[];
17
- export interface WorkResumeOptions extends DriveOptions {
18
- workspaceRoot: string;
19
- providers: RuntimeProviders;
20
- id: string;
21
- }
22
- /** Continues a work that stopped before its end, answering its questions when the caller can. */
23
- export declare function workResume(options: WorkResumeOptions): Promise<number>;
@@ -1,14 +1,13 @@
1
- import { pendingQuestions } from "@openshain/agent";
2
- import { createRuntime, isTerminal, parseWorkId, SESSION_WORK_TYPE, WorkStore, } from "@openshain/core";
1
+ import { parseWorkId, pendingApprovals, pendingQuestions, WorkStore, } from "@openshain/core";
3
2
  import { describeInput, padDisplay } from "../format.js";
4
3
  import { errorLabel, failureLabel, rejectionLabel, statusLabel } from "../labels.js";
4
+ import { nextActor } from "../report.js";
5
5
  import { formatUsage, summarizeUsage } from "../usage.js";
6
- import { drive, nextActor } from "./run.js";
7
6
  /** One line per work, oldest first. Works that cannot be read are reported, not hidden. */
8
7
  export async function workList({ workspaceRoot, write }) {
9
8
  const { works, problems } = await new WorkStore(workspaceRoot).list();
10
9
  if (works.length === 0 && problems.length === 0) {
11
- write('Work はまだありません。openshain run "<依頼>" で始められます。');
10
+ write("Work はまだありません。openshain で社員エージェントに依頼すると始まります。");
12
11
  return;
13
12
  }
14
13
  for (const work of works) {
@@ -52,6 +51,12 @@ export function describeWork(work, events) {
52
51
  for (const { question } of pendingQuestions(events))
53
52
  lines.push(`質問 ${question}`);
54
53
  }
54
+ if (work.status === "waiting_approval") {
55
+ for (const a of pendingApprovals(events)) {
56
+ const who = a.kind === "review" ? `${a.reviewer?.role ?? "資格者"}の判断待ち` : "承認待ち";
57
+ lines.push(`${who} ${a.call.name} ${describeInput(a.call.input)} (${a.approvalId})`);
58
+ }
59
+ }
55
60
  const calls = toolLines(events);
56
61
  if (calls.length > 0) {
57
62
  lines.push("Tool");
@@ -62,24 +67,6 @@ export function describeWork(work, events) {
62
67
  lines.push(nextActor(work));
63
68
  return lines;
64
69
  }
65
- /** Continues a work that stopped before its end, answering its questions when the caller can. */
66
- export async function workResume(options) {
67
- const workId = parseWorkId(options.id);
68
- const work = await new WorkStore(options.workspaceRoot).get(workId);
69
- if (work.type === SESSION_WORK_TYPE) {
70
- options.write(`${work.id} は会話の記録のため、再開できません。会話は openshain で始め直してください。`);
71
- return 1;
72
- }
73
- if (isTerminal(work.status)) {
74
- options.write(`${work.id} は${statusLabel(work.status)}のため、再開できません。`);
75
- return 1;
76
- }
77
- const runtime = await createRuntime({
78
- workspaceRoot: options.workspaceRoot,
79
- providers: options.providers,
80
- });
81
- return drive(runtime, workId, options);
82
- }
83
70
  /** One line per tool call, in log order, with its outcome when it was rejected or failed. */
84
71
  function toolLines(events) {
85
72
  const lines = new Map();
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export { AGENTS_TEMPLATE, CLAUDE_TEMPLATE, CONFIG_TEMPLATE, type InitOptions, init, MCP_TEMPLATE, } from "./commands/init.ts";
2
2
  export { type McpOptions, mcp } from "./commands/mcp.ts";
3
- export { type DriveOptions, drive, nextActor, type RunOptions, report, run, } from "./commands/run.ts";
4
3
  export { type ToolsListOptions, toolsList } from "./commands/tools.ts";
5
- export { describeWork, type WorkListOptions, type WorkResumeOptions, type WorkShowOptions, workList, workResume, workShow, } from "./commands/work.ts";
4
+ export { describeWork, type WorkListOptions, type WorkShowOptions, workList, workShow, } from "./commands/work.ts";
6
5
  export { ERROR_LABELS, errorLabel, FAILURE_LABELS, failureLabel, REJECTION_LABELS, rejectionLabel, STATUS_LABELS, statusLabel, } from "./labels.ts";
6
+ export { nextActor, progressLine, report } from "./report.ts";
7
7
  export { formatUsage, summarizeUsage, type UsageSummary } from "./usage.ts";
8
8
  export { findWorkspace } from "./workspace.ts";
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // openshain: Reference CLI agent for the openshain runtime
2
2
  export { AGENTS_TEMPLATE, CLAUDE_TEMPLATE, CONFIG_TEMPLATE, init, MCP_TEMPLATE, } from "./commands/init.js";
3
3
  export { mcp } from "./commands/mcp.js";
4
- export { drive, nextActor, report, run, } from "./commands/run.js";
5
4
  export { toolsList } from "./commands/tools.js";
6
- export { describeWork, workList, workResume, workShow, } from "./commands/work.js";
5
+ export { describeWork, workList, workShow, } from "./commands/work.js";
7
6
  export { ERROR_LABELS, errorLabel, FAILURE_LABELS, failureLabel, REJECTION_LABELS, rejectionLabel, STATUS_LABELS, statusLabel, } from "./labels.js";
7
+ export { nextActor, progressLine, report } from "./report.js";
8
8
  export { formatUsage, summarizeUsage } from "./usage.js";
9
9
  export { findWorkspace } from "./workspace.js";
package/dist/labels.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { FailureReason } from "@openshain/agent";
2
- import type { ErrorCode, ToolRejectionCode, WorkStatus } from "@openshain/core";
1
+ import type { ErrorCode, FailureReason, ToolRejectionCode, WorkStatus } from "@openshain/core";
3
2
  /** The words shown for a work's status. The log keeps the original value. */
4
3
  export declare const STATUS_LABELS: Record<WorkStatus, string>;
5
4
  /** Why a work failed, as a heading before the original detail. */
package/dist/labels.js CHANGED
@@ -23,6 +23,9 @@ export const REJECTION_LABELS = {
23
23
  reserved_path: "予約されたパス",
24
24
  outside_workspace: "workspace の外",
25
25
  invalid_path: "不正なパス",
26
+ limit_reached: "Tool 呼び出しの上限",
27
+ denied: "権限の表で不許可",
28
+ rejected_by_person: "承認されなかった",
26
29
  };
27
30
  /** A heading for a runtime error, before the original message. */
28
31
  export const ERROR_LABELS = {
@@ -0,0 +1,13 @@
1
+ export interface PreviewLine {
2
+ kind: "added" | "removed" | "context" | "note";
3
+ text: string;
4
+ }
5
+ /**
6
+ * What a held call would change, for the person about to approve it. A call of a tool that writes
7
+ * a whole file is shown as a line diff against the file as it is now; anything else is shown as
8
+ * its input. Reads the file directly: this is the person's own workspace, on their own screen.
9
+ */
10
+ export declare function previewCall(workspaceRoot: string, call: {
11
+ name: string;
12
+ input: unknown;
13
+ }): Promise<PreviewLine[]>;
@@ -0,0 +1,141 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolveWorkspacePath } from "@openshain/core";
3
+ import { csvText } from "@openshain/tools";
4
+ /** How much of a change the screen shows before it says the rest is cut. */
5
+ const MAX_LINES = 24;
6
+ /** Above this many lines on either side, the diff is replaced by the line counts. */
7
+ const MAX_DIFF_LINES = 400;
8
+ /** A single line longer than this is cut: one line must not fill the screen. */
9
+ const MAX_LINE_CHARS = 300;
10
+ /** The tools whose input the screen can render as the file it would leave behind. */
11
+ const DIFFABLE = new Set(["fs_write", "csv_write"]);
12
+ /**
13
+ * What a held call would change, for the person about to approve it. A call of a tool that writes
14
+ * a whole file is shown as a line diff against the file as it is now; anything else is shown as
15
+ * its input. Reads the file directly: this is the person's own workspace, on their own screen.
16
+ */
17
+ export async function previewCall(workspaceRoot, call) {
18
+ if (!DIFFABLE.has(call.name))
19
+ return [{ kind: "note", text: JSON.stringify(call.input) }];
20
+ const input = (call.input ?? {});
21
+ const path = typeof input.path === "string" ? input.path : undefined;
22
+ const content = typeof input.content === "string"
23
+ ? input.content
24
+ : Array.isArray(input.rows)
25
+ ? csvText(input.rows, Array.isArray(input.columns) ? input.columns : undefined)
26
+ : undefined;
27
+ if (path === undefined || content === undefined) {
28
+ return [{ kind: "note", text: JSON.stringify(call.input) }];
29
+ }
30
+ // The same guard the tools run under: a path outside the workspace, a reserved one or a
31
+ // symlink that leads out is refused here too, so the screen never shows what the call cannot
32
+ // touch. The model chooses this path; the person is about to read what it says.
33
+ let resolved;
34
+ try {
35
+ resolved = await resolveWorkspacePath(workspaceRoot, path);
36
+ }
37
+ catch (err) {
38
+ return [
39
+ {
40
+ kind: "note",
41
+ text: `${path} は読めません(${err instanceof Error ? err.message : String(err)})。この呼び出しは実行しても拒否されます。`,
42
+ },
43
+ ];
44
+ }
45
+ const before = await readFile(resolved, "utf8").catch(() => undefined);
46
+ if (before === undefined) {
47
+ const lines = content.split("\n");
48
+ return cap([
49
+ { kind: "note", text: `${path} を新しく作ります(${lines.length} 行)` },
50
+ ...lines.map((text) => ({ kind: "added", text })),
51
+ ]);
52
+ }
53
+ const oldLines = before.split("\n");
54
+ const newLines = content.split("\n");
55
+ if (oldLines.length > MAX_DIFF_LINES || newLines.length > MAX_DIFF_LINES) {
56
+ return [
57
+ {
58
+ kind: "note",
59
+ text: `${path} を書き換えます(${oldLines.length} 行 → ${newLines.length} 行。大きいので差分は出しません)`,
60
+ },
61
+ ];
62
+ }
63
+ const body = diff(oldLines, newLines);
64
+ return cap([{ kind: "note", text: `${path} を書き換えます` }, ...body]);
65
+ }
66
+ function cap(lines) {
67
+ const short = lines.map((line) => line.text.length > MAX_LINE_CHARS
68
+ ? { ...line, text: `${line.text.slice(0, MAX_LINE_CHARS)}…` }
69
+ : line);
70
+ if (short.length <= MAX_LINES)
71
+ return short;
72
+ const rest = short.length - MAX_LINES;
73
+ return [...short.slice(0, MAX_LINES), { kind: "note", text: `ほか ${rest} 行` }];
74
+ }
75
+ /**
76
+ * A line diff by the longest common subsequence, with the unchanged lines around a change kept
77
+ * as context. Small files only; the caller checks the size first.
78
+ */
79
+ function diff(before, after) {
80
+ const table = Array.from({ length: before.length + 1 }, () => new Array(after.length + 1).fill(0));
81
+ for (let i = before.length - 1; i >= 0; i--) {
82
+ for (let j = after.length - 1; j >= 0; j--) {
83
+ const row = table[i];
84
+ const next = table[i + 1];
85
+ row[j] =
86
+ before[i] === after[j]
87
+ ? next[j + 1] + 1
88
+ : Math.max(next[j], row[j + 1]);
89
+ }
90
+ }
91
+ const all = [];
92
+ let i = 0;
93
+ let j = 0;
94
+ while (i < before.length && j < after.length) {
95
+ if (before[i] === after[j]) {
96
+ all.push({ kind: "context", text: before[i] });
97
+ i++;
98
+ j++;
99
+ }
100
+ else if ((table[i + 1]?.[j] ?? 0) >= (table[i]?.[j + 1] ?? 0)) {
101
+ all.push({ kind: "removed", text: before[i] });
102
+ i++;
103
+ }
104
+ else {
105
+ all.push({ kind: "added", text: after[j] });
106
+ j++;
107
+ }
108
+ }
109
+ for (; i < before.length; i++)
110
+ all.push({ kind: "removed", text: before[i] });
111
+ for (; j < after.length; j++)
112
+ all.push({ kind: "added", text: after[j] });
113
+ return trimContext(all);
114
+ }
115
+ /** Keeps two unchanged lines on each side of a change and marks what was left out. */
116
+ function trimContext(lines, keep = 2) {
117
+ const wanted = new Set();
118
+ lines.forEach((line, index) => {
119
+ if (line.kind === "context")
120
+ return;
121
+ for (let k = index - keep; k <= index + keep; k++)
122
+ wanted.add(k);
123
+ });
124
+ const out = [];
125
+ let skipped = 0;
126
+ lines.forEach((line, index) => {
127
+ if (wanted.has(index)) {
128
+ if (skipped > 0) {
129
+ out.push({ kind: "note", text: `… 変わらない ${skipped} 行 …` });
130
+ skipped = 0;
131
+ }
132
+ out.push(line);
133
+ }
134
+ else {
135
+ skipped++;
136
+ }
137
+ });
138
+ if (skipped > 0)
139
+ out.push({ kind: "note", text: `… 変わらない ${skipped} 行 …` });
140
+ return out;
141
+ }
@@ -0,0 +1,6 @@
1
+ import { type AnyEvent, type Work } from "@openshain/core";
2
+ /** One line for a tool call, a rejection or a failure; nothing for the other events. `names` maps call ids to tool names. */
3
+ export declare function progressLine(event: AnyEvent, names: Map<string, string>): string | undefined;
4
+ /** The closing lines: what happened, what it cost, and who acts next. */
5
+ export declare function report(work: Work, events: AnyEvent[]): string[];
6
+ export declare function nextActor(work: Work): string;
@@ -1,39 +1,7 @@
1
- import { pendingQuestions, runWork } from "@openshain/agent";
2
- import { createRuntime, } from "@openshain/core";
3
- import { describeInput, truncate } from "../format.js";
4
- import { failureLabel, rejectionLabel, statusLabel } from "../labels.js";
5
- import { formatUsage, summarizeUsage } from "../usage.js";
6
- /** Creates a work for the request and drives it. Exit code 0 when the work completed. */
7
- export async function run(options) {
8
- const runtime = await createRuntime({
9
- workspaceRoot: options.workspaceRoot,
10
- providers: options.providers,
11
- });
12
- const work = await runtime.works.create({
13
- objective: options.objective,
14
- principal: runtime.config.principal.id,
15
- profession: runtime.config.profession.id,
16
- });
17
- options.write(`${work.id} を開始`);
18
- return drive(runtime, work.id, options);
19
- }
20
- /** Drives a work from its current state, printing one line per tool call, and closes with the report. */
21
- export async function drive(runtime, workId, options) {
22
- const names = new Map();
23
- const done = await runWork(runtime, workId, {
24
- ...(options.ask && { onInput: options.ask }),
25
- ...(options.signal && { signal: options.signal }),
26
- onEvent: (event) => {
27
- const line = progressLine(event, names);
28
- if (line)
29
- options.write(line);
30
- },
31
- });
32
- const events = await runtime.works.events(workId);
33
- for (const line of report(done, events))
34
- options.write(line);
35
- return done.status === "completed" ? 0 : 1;
36
- }
1
+ import { pendingQuestions, } from "@openshain/core";
2
+ import { describeInput, truncate } from "./format.js";
3
+ import { failureLabel, rejectionLabel, statusLabel } from "./labels.js";
4
+ import { formatUsage, summarizeUsage } from "./usage.js";
37
5
  /** One line for a tool call, a rejection or a failure; nothing for the other events. `names` maps call ids to tool names. */
38
6
  export function progressLine(event, names) {
39
7
  switch (event.type) {
@@ -94,9 +62,9 @@ export function nextActor(work) {
94
62
  case "cancelled":
95
63
  return "次に動く人はいません。";
96
64
  case "waiting_input":
97
- return `次は利用者の番です。openshain work resume ${work.id} で質問に答えると続きます。`;
65
+ return `次は利用者の番です。openshain の会話で /work resume ${work.id} を実行し、続きを依頼すると質問に答えられます。`;
98
66
  case "waiting_approval":
99
- return "次は利用者の番です。承認が要ります。";
67
+ return "次は利用者の番です。承認が要ります。openshain の会話で /approvals を確かめ、/approve <id> か /reject <id> で決めます。";
100
68
  case "failed":
101
69
  return "次は利用者の番です。原因を修正して、もう一度依頼してください。";
102
70
  default:
package/dist/tui/app.js CHANGED
@@ -15,7 +15,10 @@ const COLORS = {
15
15
  };
16
16
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
17
17
  /** Rows that are not history: the header, the input box (three rows) and the status line. */
18
+ /** The rows around the conversation: the header, the input box (3) and the status line. */
18
19
  const CHROME_ROWS = 5;
20
+ /** The approval palette is taller: its two lines of heading plus one row per choice. */
21
+ const APPROVAL_EXTRA_ROWS = 2;
19
22
  function statusText(state, scrolled) {
20
23
  if (scrolled > 0)
21
24
  return `↑ ${scrolled} 行上を表示中。End で最新へ、ホイールか PageUp と PageDown で移動`;
@@ -64,9 +67,12 @@ export function App({ controller }) {
64
67
  return () => clearInterval(timer);
65
68
  }, [state.busy]);
66
69
  // One row is left to the terminal: drawing exactly its height makes it scroll on every redraw.
67
- const height = Math.max(CHROME_ROWS + 1, size.rows - 1);
70
+ const chromeRows = (state.approval
71
+ ? CHROME_ROWS + APPROVAL_EXTRA_ROWS + state.approval.choices.length
72
+ : CHROME_ROWS) + (state.queued.length > 0 && !state.approval ? 1 : 0);
73
+ const height = Math.max(chromeRows + 1, size.rows - 1);
68
74
  const width = Math.max(20, size.columns);
69
- const paneRows = height - CHROME_ROWS;
75
+ const paneRows = height - chromeRows;
70
76
  const lines = useMemo(() => screenLines(state.entries, width).map((line, row) => ({ ...line, row })), [state.entries, width]);
71
77
  const maxScroll = Math.max(0, lines.length - paneRows);
72
78
  const scrolled = Math.min(scroll, maxScroll);
@@ -93,6 +99,22 @@ export function App({ controller }) {
93
99
  void controller.close();
94
100
  return;
95
101
  }
102
+ // While a call waits for approval, the keys pick a choice instead of typing.
103
+ if (state.approval) {
104
+ if (key.upArrow)
105
+ return controller.moveApproval(-1);
106
+ if (key.downArrow)
107
+ return controller.moveApproval(1);
108
+ if (key.return)
109
+ return controller.decideApproval();
110
+ if (key.escape)
111
+ return controller.decideApproval("reject");
112
+ const index = Number.parseInt(ch, 10) - 1;
113
+ const chosen = state.approval.choices[index];
114
+ if (chosen)
115
+ return controller.decideApproval(chosen.key);
116
+ return;
117
+ }
96
118
  if (key.pageUp)
97
119
  return setScroll((s) => Math.min(maxScroll, s + page));
98
120
  if (key.pageDown)
@@ -167,7 +189,9 @@ export function App({ controller }) {
167
189
  setInput([...chars.slice(0, at), ch, ...chars.slice(at)].join(""));
168
190
  setCursor(at + [...ch].length);
169
191
  });
192
+ const approval = state.approval;
170
193
  const asking = state.question !== undefined;
194
+ const queued = state.queued;
171
195
  const chars = [...input];
172
196
  const at = Math.min(cursor, chars.length);
173
197
  const before = chars.slice(0, at).join("");
@@ -183,9 +207,18 @@ export function App({ controller }) {
183
207
  state.status.model,
184
208
  ].join(" · ") }), _jsx(Box, { flexDirection: "column", height: paneRows, children: visible.map((line) => {
185
209
  const color = COLORS[line.kind];
186
- if (line.segments) {
187
- return (_jsx(Text, { wrap: "truncate", children: line.segments.map((s) => (_jsx(Text, { color: s.color, children: s.text }, s.at))) }, line.row));
210
+ if (line.spans) {
211
+ // Each piece is named by the column it starts at, which does not move.
212
+ let column = 0;
213
+ const pieces = line.spans.map((span) => {
214
+ const at = column;
215
+ column += span.text.length;
216
+ return { ...span, at };
217
+ });
218
+ return (_jsx(Text, { wrap: "truncate", ...(color && { color }), children: pieces.map((s) => (_jsx(Text, { ...(s.color && { color: s.color }), ...(s.bold && { bold: true }), ...(s.italic && { italic: true }), ...(s.dim && { dimColor: true }), ...(s.strikethrough && { strikethrough: true }), children: s.text }, `${line.row}-${s.at}`))) }, line.row));
188
219
  }
189
220
  return (_jsx(Text, { wrap: "truncate", dimColor: line.kind === "banner", ...(color && { color }), children: line.text || " " }, line.row));
190
- }) }), _jsxs(Box, { borderStyle: "round", borderColor: asking ? "magenta" : "gray", paddingX: 1, children: [_jsx(Text, { color: asking ? "magenta" : "cyan", children: asking ? "答え> " : "> " }), _jsx(Text, { children: before }), under === "" ? _jsx(Text, { dimColor: true, children: "\u258C" }) : _jsx(Text, { inverse: true, children: under }), _jsx(Text, { children: after })] }), _jsx(Text, { dimColor: true, wrap: "truncate", children: bottom })] }));
221
+ }) }), approval ? (_jsxs(Box, { borderStyle: "round", borderColor: "yellow", paddingX: 1, flexDirection: "column", children: [_jsxs(Text, { color: "yellow", wrap: "truncate", children: ["\u627F\u8A8D\u304C\u8981\u308A\u307E\u3059: ", approval.title] }), _jsxs(Text, { dimColor: true, wrap: "truncate", children: ["\u898F\u5247 ", approval.ruleId] }), approval.choices.map((choice, index) => (_jsxs(Text, { ...(index === approval.at && { color: "yellow" }), wrap: "truncate", children: [index === approval.at ? "❯ " : " ", index + 1, ". ", choice.label] }, choice.key)))] })) : (_jsxs(Box, { borderStyle: "round", borderColor: asking ? "magenta" : "gray", paddingX: 1, children: [_jsx(Text, { color: asking ? "magenta" : "cyan", children: asking ? "答え> " : "> " }), _jsx(Text, { children: before }), under === "" ? _jsx(Text, { dimColor: true, children: "\u258C" }) : _jsx(Text, { inverse: true, children: under }), _jsx(Text, { children: after })] })), queued.length > 0 && !approval ? (_jsxs(Text, { dimColor: true, wrap: "truncate", children: ["\u9806\u756A\u5F85\u3061 ", queued.length, " \u4EF6: ", queued.join(" / ")] })) : null, _jsx(Text, { dimColor: true, wrap: "truncate", children: approval
222
+ ? "↑ ↓ と Enter、または数字で選ぶ。Esc は拒否します。Ctrl-C は保留のまま止めます"
223
+ : bottom })] }));
191
224
  }
@@ -1,3 +1,4 @@
1
+ import type { Span } from "./markdown.ts";
1
2
  /** The version of the openshain command, from its package.json. */
2
3
  export declare const VERSION: string;
3
4
  /**
@@ -5,11 +6,5 @@ export declare const VERSION: string;
5
6
  * screen needs neither the network nor another dependency to show it.
6
7
  */
7
8
  export declare const LOGO_ROWS: readonly string[];
8
- export interface Segment {
9
- text: string;
10
- color: string;
11
- /** Position in the row; the screen keys by it. */
12
- at: number;
13
- }
14
- /** One colored segment per character, so the gradient runs across the row. */
15
- export declare function logoSegments(row: string): Segment[];
9
+ /** One colored piece per character, so the gradient runs across the row. */
10
+ export declare function logoSegments(row: string): Span[];
@@ -15,7 +15,7 @@ const GRADIENT = [
15
15
  [78, 168, 255],
16
16
  [127, 136, 255],
17
17
  ];
18
- /** One colored segment per character, so the gradient runs across the row. */
18
+ /** One colored piece per character, so the gradient runs across the row. */
19
19
  export function logoSegments(row) {
20
20
  const chars = [...row];
21
21
  const last = Math.max(1, chars.length - 1);
@@ -24,6 +24,6 @@ export function logoSegments(row) {
24
24
  const [from, to] = GRADIENT;
25
25
  const channel = (k) => Math.round(from[k] + (to[k] - from[k]) * t);
26
26
  const hex = [channel(0), channel(1), channel(2)].map((v) => v.toString(16).padStart(2, "0"));
27
- return { text, color: `#${hex.join("")}`, at };
27
+ return { text, color: `#${hex.join("")}` };
28
28
  });
29
29
  }