openshain 0.1.1 → 0.3.1
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/dist/bin.d.ts +2 -0
- package/dist/bin.js +108 -0
- package/dist/commands/init.d.ts +21 -0
- package/dist/commands/init.js +128 -0
- package/dist/commands/mcp.d.ts +10 -0
- package/dist/commands/mcp.js +17 -0
- package/dist/commands/tools.d.ts +8 -0
- package/dist/commands/tools.js +18 -0
- package/dist/commands/work.d.ts +15 -0
- package/dist/commands/work.js +87 -0
- package/dist/format.d.ts +15 -0
- package/dist/format.js +103 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +9 -0
- package/dist/labels.d.ts +13 -0
- package/dist/labels.js +59 -0
- package/dist/report.d.ts +6 -0
- package/dist/report.js +73 -0
- package/dist/tui/app.d.ts +9 -0
- package/dist/tui/app.js +191 -0
- package/dist/tui/banner.d.ts +15 -0
- package/dist/tui/banner.js +29 -0
- package/dist/tui/controller.d.ts +59 -0
- package/dist/tui/controller.js +325 -0
- package/dist/tui/index.d.ts +7 -0
- package/dist/tui/index.js +45 -0
- package/dist/tui/lines.d.ts +12 -0
- package/dist/tui/lines.js +68 -0
- package/dist/usage.d.ts +12 -0
- package/dist/usage.js +32 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.js +20 -0
- package/package.json +17 -7
- package/src/bin.ts +5 -39
- package/src/commands/init.ts +9 -9
- package/src/commands/tools.ts +7 -2
- package/src/commands/work.ts +3 -34
- package/src/index.ts +1 -10
- package/src/labels.ts +2 -2
- package/src/{commands/run.ts → report.ts} +6 -59
- package/src/tui/controller.ts +113 -75
- package/src/tui/index.ts +3 -1
- package/src/usage.ts +1 -2
- package/src/workspace.ts +1 -1
package/dist/bin.d.ts
ADDED
package/dist/bin.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { anthropicProvider, openaiCompatibleProvider } from "@openshain/agent";
|
|
4
|
+
import { isOpenshainError } from "@openshain/core";
|
|
5
|
+
import { standardTools } from "@openshain/tools";
|
|
6
|
+
import { init } from "./commands/init.js";
|
|
7
|
+
import { mcp } from "./commands/mcp.js";
|
|
8
|
+
import { toolsList } from "./commands/tools.js";
|
|
9
|
+
import { workList, workShow } from "./commands/work.js";
|
|
10
|
+
import { plain } from "./format.js";
|
|
11
|
+
import { errorLabel } from "./labels.js";
|
|
12
|
+
import { startTui } from "./tui/index.js";
|
|
13
|
+
import { findWorkspace } from "./workspace.js";
|
|
14
|
+
const USAGE = `使い方:
|
|
15
|
+
openshain 端末で対話を始める
|
|
16
|
+
openshain init openshain.yaml のひな型を書く
|
|
17
|
+
openshain tools list 使える Tool の一覧
|
|
18
|
+
openshain work list Work の一覧
|
|
19
|
+
openshain work show <id> Work の詳細
|
|
20
|
+
openshain mcp MCP Server を stdio で起動する
|
|
21
|
+
|
|
22
|
+
--workspace <dir> 起点のディレクトリ。省略時はカレントディレクトリ
|
|
23
|
+
init はそこに書き、他のコマンドはそこから上に openshain.yaml を探す`;
|
|
24
|
+
/** The providers this CLI knows, by the ids used in openshain.yaml. */
|
|
25
|
+
const providers = {
|
|
26
|
+
models: {
|
|
27
|
+
anthropic: (model) => anthropicProvider(model),
|
|
28
|
+
"openai-compatible": (model) => openaiCompatibleProvider(model),
|
|
29
|
+
},
|
|
30
|
+
tools: { standard: () => standardTools() },
|
|
31
|
+
};
|
|
32
|
+
async function main(argv) {
|
|
33
|
+
const write = (line) => console.log(plain(line));
|
|
34
|
+
let values;
|
|
35
|
+
let positionals;
|
|
36
|
+
try {
|
|
37
|
+
({ values, positionals } = parseArgs({
|
|
38
|
+
args: argv,
|
|
39
|
+
options: { workspace: { type: "string" }, help: { type: "boolean", short: "h" } },
|
|
40
|
+
allowPositionals: true,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
45
|
+
const option = /'(-[^']*)'/.exec(message)?.[1];
|
|
46
|
+
const unknown = err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION" && option;
|
|
47
|
+
write(unknown ? `不明なオプション ${option}` : `引数を解釈できません。${message}`);
|
|
48
|
+
write(USAGE);
|
|
49
|
+
return 2;
|
|
50
|
+
}
|
|
51
|
+
const [command, ...rest] = positionals;
|
|
52
|
+
if (!command && !values.help && process.stdin.isTTY === true && process.stdout.isTTY === true) {
|
|
53
|
+
const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
|
|
54
|
+
return startTui({ workspaceRoot, providers });
|
|
55
|
+
}
|
|
56
|
+
if (values.help || !command) {
|
|
57
|
+
write(USAGE);
|
|
58
|
+
return values.help ? 0 : 2;
|
|
59
|
+
}
|
|
60
|
+
switch (command) {
|
|
61
|
+
case "init":
|
|
62
|
+
await init({ workspaceRoot: values.workspace ?? process.cwd(), write });
|
|
63
|
+
return 0;
|
|
64
|
+
case "mcp": {
|
|
65
|
+
const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
|
|
66
|
+
await mcp({ workspaceRoot, providers });
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
case "tools": {
|
|
70
|
+
if (rest[0] !== "list") {
|
|
71
|
+
write(USAGE);
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
|
|
75
|
+
await toolsList({ workspaceRoot, providers, write });
|
|
76
|
+
return 0;
|
|
77
|
+
}
|
|
78
|
+
case "work": {
|
|
79
|
+
const sub = rest[0];
|
|
80
|
+
const id = rest[1] ?? "";
|
|
81
|
+
if (!(sub === "list" || (sub === "show" && id))) {
|
|
82
|
+
write(USAGE);
|
|
83
|
+
return 2;
|
|
84
|
+
}
|
|
85
|
+
const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
|
|
86
|
+
if (sub === "list") {
|
|
87
|
+
await workList({ workspaceRoot, write });
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
await workShow({ workspaceRoot, id, write });
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
write(`不明なコマンド ${command}`);
|
|
95
|
+
write(USAGE);
|
|
96
|
+
return 2;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
main(process.argv.slice(2)).then((code) => process.exit(code), (err) => {
|
|
100
|
+
if (isOpenshainError(err)) {
|
|
101
|
+
const heading = errorLabel(err.code);
|
|
102
|
+
console.error(plain(`エラー(${err.code}) ${heading ? `${heading}。` : ""}${err.message}`));
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.error(err);
|
|
106
|
+
}
|
|
107
|
+
process.exit(1);
|
|
108
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type Language } from "@openshain/core";
|
|
2
|
+
/** The company's language for the template: from the OS locale, Japanese unless the locale says otherwise. */
|
|
3
|
+
export declare function detectLanguage(env: Record<string, string | undefined>): Language;
|
|
4
|
+
export declare const configTemplate: (language: Language) => string;
|
|
5
|
+
export declare const CONFIG_TEMPLATE: string;
|
|
6
|
+
/** Registers the runtime as a project MCP server for Claude Code. `openshain` must be on PATH. */
|
|
7
|
+
export declare const MCP_TEMPLATE: string;
|
|
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\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` \u3068 `work/` \u306F Runtime \u306E\u3082\u306E\u3002\u5909\u66F4\u3057\u306A\u3044\n";
|
|
10
|
+
export declare const CLAUDE_TEMPLATE = "@AGENTS.md\n";
|
|
11
|
+
export interface InitOptions {
|
|
12
|
+
workspaceRoot: string;
|
|
13
|
+
write: (line: string) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Writes the starter files of a company workspace: openshain.yaml, .mcp.json, AGENTS.md and
|
|
17
|
+
* CLAUDE.md. A workspace that already has openshain.yaml is left alone with an error. An existing
|
|
18
|
+
* .mcp.json keeps its servers and gains the openshain entry when it lacks one; any other file that
|
|
19
|
+
* already exists is kept as it is.
|
|
20
|
+
*/
|
|
21
|
+
export declare function init({ workspaceRoot, write }: InitOptions): Promise<void>;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { CONFIG_FILE_NAME, OpenshainError } from "@openshain/core";
|
|
4
|
+
/** The company's language for the template: from the OS locale, Japanese unless the locale says otherwise. */
|
|
5
|
+
export function detectLanguage(env) {
|
|
6
|
+
const raw = env.LC_ALL || env.LC_MESSAGES || env.LANG || "";
|
|
7
|
+
const code = raw.toLowerCase().split(/[_.@-]/)[0] ?? "";
|
|
8
|
+
if (code === "ja" || code === "" || code === "c" || code === "posix")
|
|
9
|
+
return "ja";
|
|
10
|
+
return "en";
|
|
11
|
+
}
|
|
12
|
+
export const configTemplate = (language) => `version: 1
|
|
13
|
+
company:
|
|
14
|
+
name: サンプル株式会社 # 会社名。model に伝わる
|
|
15
|
+
language: ${language} # ja | en。社員エージェントの名前の言語。init が OS の locale から埋める
|
|
16
|
+
principal:
|
|
17
|
+
id: alice # 依頼する人の id。小文字の英数字、_ と -
|
|
18
|
+
name: Alice
|
|
19
|
+
profession:
|
|
20
|
+
id: generic
|
|
21
|
+
# model への指示。100,000 文字まで
|
|
22
|
+
instructions: |
|
|
23
|
+
あなたはこの会社の事務担当です。依頼された作業を、workspace 内のファイルだけを使って進めてください。
|
|
24
|
+
model:
|
|
25
|
+
provider: anthropic # anthropic | openai-compatible
|
|
26
|
+
model: claude-opus-5
|
|
27
|
+
api_key_env: ANTHROPIC_API_KEY # API キーを入れておく環境変数の名前。サーバーを替えるなら変数名も見直す
|
|
28
|
+
# base_url: http://localhost:11434/v1 # openai-compatible のとき
|
|
29
|
+
# options: { effort: high } # provider にそのまま渡す
|
|
30
|
+
tools:
|
|
31
|
+
- provider: standard
|
|
32
|
+
# allow: [fs_list, fs_search, fs_read, csv_read, csv_aggregate, markdown_read, fs_write, csv_write] # 省略時は全部
|
|
33
|
+
# - module: ./tools/my-tool.ts # ToolProvider を default export するモジュール
|
|
34
|
+
limits:
|
|
35
|
+
max_model_calls: 30 # 超えると Work は失敗(上限到達)で止まる
|
|
36
|
+
max_tool_calls: 100
|
|
37
|
+
max_output_tokens: 16000 # model の 1 回の出力の上限
|
|
38
|
+
# debug:
|
|
39
|
+
# persist_raw: true # provider の生の応答を記録に残す
|
|
40
|
+
`;
|
|
41
|
+
export const CONFIG_TEMPLATE = configTemplate("ja");
|
|
42
|
+
/** Registers the runtime as a project MCP server for Claude Code. `openshain` must be on PATH. */
|
|
43
|
+
export const MCP_TEMPLATE = `${JSON.stringify({ mcpServers: { openshain: { command: "openshain", args: ["mcp"] } } }, null, 2)}\n`;
|
|
44
|
+
/** What an outside agent reads before working in the folder. Codex reads AGENTS.md; Claude Code reads it through CLAUDE.md. */
|
|
45
|
+
export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェントへ
|
|
46
|
+
|
|
47
|
+
このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP 経由でこのフォルダを扱うときのものです。openshain の対話型 CLI も同じ手順で Runtime を使います。
|
|
48
|
+
|
|
49
|
+
会社のファイルの読み書きと集計は openshain の MCP tool で行います。Claude Code や Codex 自身の Read、Write、Bash は会社のファイルには使いません。Runtime を通らなかった操作は記録に残らないためです。
|
|
50
|
+
|
|
51
|
+
- 依頼を受けたら、まず \`work_create\` に依頼の文をそのまま渡して Work を作る
|
|
52
|
+
- ファイルは \`fs_list\`、\`fs_search\`、\`fs_read\`、\`csv_read\`、\`markdown_read\` で見る。合計や件数は \`csv_aggregate\` に任せ、自分で合計しない
|
|
53
|
+
- 書くときは \`fs_write\` か \`csv_write\`
|
|
54
|
+
- 終わったら \`work_complete\` に、何をしたかと、書いたファイルを渡す。続けられないときは \`work_fail\`
|
|
55
|
+
- \`openshain.yaml\` と \`work/\` は Runtime のもの。変更しない
|
|
56
|
+
`;
|
|
57
|
+
export const CLAUDE_TEMPLATE = "@AGENTS.md\n";
|
|
58
|
+
/**
|
|
59
|
+
* Writes the starter files of a company workspace: openshain.yaml, .mcp.json, AGENTS.md and
|
|
60
|
+
* CLAUDE.md. A workspace that already has openshain.yaml is left alone with an error. An existing
|
|
61
|
+
* .mcp.json keeps its servers and gains the openshain entry when it lacks one; any other file that
|
|
62
|
+
* already exists is kept as it is.
|
|
63
|
+
*/
|
|
64
|
+
export async function init({ workspaceRoot, write }) {
|
|
65
|
+
const configPath = join(workspaceRoot, CONFIG_FILE_NAME);
|
|
66
|
+
try {
|
|
67
|
+
await writeFile(configPath, configTemplate(detectLanguage(process.env)), { flag: "wx" });
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const code = err.code;
|
|
71
|
+
if (code === "EEXIST") {
|
|
72
|
+
throw new OpenshainError("config", `${configPath} はすでにあります。上書きはしません。`);
|
|
73
|
+
}
|
|
74
|
+
if (code === "ENOENT") {
|
|
75
|
+
throw new OpenshainError("config", `${workspaceRoot} がありません。先にディレクトリを作ってください。`);
|
|
76
|
+
}
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
write(`${configPath} を作りました。`);
|
|
80
|
+
for (const [name, content] of [
|
|
81
|
+
[".mcp.json", MCP_TEMPLATE],
|
|
82
|
+
["AGENTS.md", AGENTS_TEMPLATE],
|
|
83
|
+
["CLAUDE.md", CLAUDE_TEMPLATE],
|
|
84
|
+
]) {
|
|
85
|
+
const path = join(workspaceRoot, name);
|
|
86
|
+
try {
|
|
87
|
+
await writeFile(path, content, { flag: "wx" });
|
|
88
|
+
write(`${path} を作りました。`);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (err.code !== "EEXIST")
|
|
92
|
+
throw err;
|
|
93
|
+
if (name === ".mcp.json")
|
|
94
|
+
write(await addToMcpConfig(path));
|
|
95
|
+
else
|
|
96
|
+
write(`${path} はすでにあるので変更しません。`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
write("company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain を実行してください。");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Adds the openshain server to a .mcp.json that is already there, keeping every other server.
|
|
103
|
+
* A file that cannot be read as JSON is left as it is, and the line says so.
|
|
104
|
+
*/
|
|
105
|
+
async function addToMcpConfig(path) {
|
|
106
|
+
let parsed;
|
|
107
|
+
try {
|
|
108
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return `${path} はすでにあり、JSON として読めないので変更しません。openshain を手動で登録してください。`;
|
|
112
|
+
}
|
|
113
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
114
|
+
return `${path} はすでにあり、形が違うので変更しません。openshain を手動で登録してください。`;
|
|
115
|
+
}
|
|
116
|
+
const config = parsed;
|
|
117
|
+
const servers = config.mcpServers && typeof config.mcpServers === "object" && !Array.isArray(config.mcpServers)
|
|
118
|
+
? config.mcpServers
|
|
119
|
+
: {};
|
|
120
|
+
if (Object.hasOwn(servers, "openshain"))
|
|
121
|
+
return `${path} には openshain がすでにあるので変更しません。`;
|
|
122
|
+
const next = {
|
|
123
|
+
...config,
|
|
124
|
+
mcpServers: { ...servers, openshain: { command: "openshain", args: ["mcp"] } },
|
|
125
|
+
};
|
|
126
|
+
await writeFile(path, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
127
|
+
return `${path} に openshain を追加しました。他の項目はそのままです。`;
|
|
128
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { RuntimeProviders } from "@openshain/core";
|
|
2
|
+
export interface McpOptions {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
providers: RuntimeProviders;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Serves the workspace over MCP on stdin and stdout until the client hangs up. Nothing else may
|
|
8
|
+
* be written to stdout while it runs; the protocol owns it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function mcp({ workspaceRoot, providers }: McpOptions): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2
|
+
import { createMcpServer } from "@openshain/mcp";
|
|
3
|
+
/**
|
|
4
|
+
* Serves the workspace over MCP on stdin and stdout until the client hangs up. Nothing else may
|
|
5
|
+
* be written to stdout while it runs; the protocol owns it.
|
|
6
|
+
*/
|
|
7
|
+
export async function mcp({ workspaceRoot, providers }) {
|
|
8
|
+
const server = await createMcpServer({ workspaceRoot, tools: providers.tools });
|
|
9
|
+
const transport = new StdioServerTransport();
|
|
10
|
+
await new Promise((resolve) => {
|
|
11
|
+
server.onclose = () => resolve();
|
|
12
|
+
server.connect(transport).catch((err) => {
|
|
13
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
14
|
+
resolve();
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type RuntimeProviders } from "@openshain/core";
|
|
2
|
+
export interface ToolsListOptions {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
providers: RuntimeProviders;
|
|
5
|
+
write: (line: string) => void;
|
|
6
|
+
}
|
|
7
|
+
/** Every tool the model can call in this workspace, and the ones the allow lists hide. Needs no model provider. */
|
|
8
|
+
export declare function toolsList({ workspaceRoot, providers, write, }: ToolsListOptions): Promise<void>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ASK_USER, createToolRegistry, loadConfig, RUNTIME_PROVIDER_ID, } from "@openshain/core";
|
|
2
|
+
/** Every tool the model can call in this workspace, and the ones the allow lists hide. Needs no model provider. */
|
|
3
|
+
export async function toolsList({ workspaceRoot, providers, write, }) {
|
|
4
|
+
const config = await loadConfig(workspaceRoot);
|
|
5
|
+
const registry = await createToolRegistry(workspaceRoot, config, providers.tools);
|
|
6
|
+
const rows = registry
|
|
7
|
+
.list()
|
|
8
|
+
.map((t) => [t.definition.name, t.providerId, t.definition.effect, "許可"]);
|
|
9
|
+
rows.push([ASK_USER.name, RUNTIME_PROVIDER_ID, ASK_USER.effect, "許可"]);
|
|
10
|
+
for (const hidden of registry.hiddenTools()) {
|
|
11
|
+
rows.push([hidden.name, hidden.providerId, hidden.effect, "不許可"]);
|
|
12
|
+
}
|
|
13
|
+
const width = Math.max(...rows.map(([name]) => name.length));
|
|
14
|
+
const providerWidth = Math.max(...rows.map(([, provider]) => provider.length));
|
|
15
|
+
for (const [name, provider, effect, allowed] of rows) {
|
|
16
|
+
write(`${name.padEnd(width)} ${provider.padEnd(providerWidth)} ${effect.padEnd(7)} ${allowed}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type AnyEvent, type Work } from "@openshain/core";
|
|
2
|
+
export interface WorkListOptions {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
write: (line: string) => void;
|
|
5
|
+
}
|
|
6
|
+
/** One line per work, oldest first. Works that cannot be read are reported, not hidden. */
|
|
7
|
+
export declare function workList({ workspaceRoot, write }: WorkListOptions): Promise<void>;
|
|
8
|
+
export interface WorkShowOptions {
|
|
9
|
+
workspaceRoot: string;
|
|
10
|
+
id: string;
|
|
11
|
+
write: (line: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/** Everything about one work: state, outcome, what the tools did, the usage, and who acts next. */
|
|
14
|
+
export declare function workShow({ workspaceRoot, id, write }: WorkShowOptions): Promise<void>;
|
|
15
|
+
export declare function describeWork(work: Work, events: AnyEvent[]): string[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { parseWorkId, pendingQuestions, WorkStore, } from "@openshain/core";
|
|
2
|
+
import { describeInput, padDisplay } from "../format.js";
|
|
3
|
+
import { errorLabel, failureLabel, rejectionLabel, statusLabel } from "../labels.js";
|
|
4
|
+
import { nextActor } from "../report.js";
|
|
5
|
+
import { formatUsage, summarizeUsage } from "../usage.js";
|
|
6
|
+
/** One line per work, oldest first. Works that cannot be read are reported, not hidden. */
|
|
7
|
+
export async function workList({ workspaceRoot, write }) {
|
|
8
|
+
const { works, problems } = await new WorkStore(workspaceRoot).list();
|
|
9
|
+
if (works.length === 0 && problems.length === 0) {
|
|
10
|
+
write("Work はまだありません。openshain で社員エージェントに依頼すると始まります。");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
for (const work of works) {
|
|
14
|
+
write(`${work.id} ${padDisplay(statusLabel(work.status), 16)} ${work.createdAt.slice(0, 16)} ${shorten(work.objective)}`);
|
|
15
|
+
}
|
|
16
|
+
for (const { id, error } of problems) {
|
|
17
|
+
write(`${id} 読めない(${errorLabel(error.code) ?? error.code}) ${error.message}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** Everything about one work: state, outcome, what the tools did, the usage, and who acts next. */
|
|
21
|
+
export async function workShow({ workspaceRoot, id, write }) {
|
|
22
|
+
const store = new WorkStore(workspaceRoot);
|
|
23
|
+
const workId = parseWorkId(id);
|
|
24
|
+
const work = await store.get(workId);
|
|
25
|
+
const events = await store.events(workId);
|
|
26
|
+
for (const line of describeWork(work, events))
|
|
27
|
+
write(line);
|
|
28
|
+
}
|
|
29
|
+
export function describeWork(work, events) {
|
|
30
|
+
const lines = [
|
|
31
|
+
`${work.id}`,
|
|
32
|
+
`状態 ${statusLabel(work.status)}(${work.status})`,
|
|
33
|
+
`依頼 ${work.objective}`,
|
|
34
|
+
`作成 ${work.createdAt}`,
|
|
35
|
+
];
|
|
36
|
+
if (work.agentName)
|
|
37
|
+
lines.push(`名前 ${work.agentName}(社員エージェント)`);
|
|
38
|
+
if (work.startedAt)
|
|
39
|
+
lines.push(`開始 ${work.startedAt}`);
|
|
40
|
+
if (work.completedAt)
|
|
41
|
+
lines.push(`終了 ${work.completedAt}`);
|
|
42
|
+
if (work.outcome) {
|
|
43
|
+
lines.push(`結果 ${work.outcome.summary}`);
|
|
44
|
+
for (const artifact of work.outcome.artifacts)
|
|
45
|
+
lines.push(` 書き込み ${artifact.path} ${artifact.sha256.slice(0, 12)}${artifact.missing ? " 完了時には読めなかった" : ""}${artifact.claimed ? " エージェントの申告(この Work の Tool は書いていない)" : ""}`);
|
|
46
|
+
}
|
|
47
|
+
if (work.failure) {
|
|
48
|
+
lines.push(`失敗 ${failureLabel(work.failure.reason)}。${work.failure.detail}`);
|
|
49
|
+
}
|
|
50
|
+
if (work.status === "waiting_input") {
|
|
51
|
+
for (const { question } of pendingQuestions(events))
|
|
52
|
+
lines.push(`質問 ${question}`);
|
|
53
|
+
}
|
|
54
|
+
const calls = toolLines(events);
|
|
55
|
+
if (calls.length > 0) {
|
|
56
|
+
lines.push("Tool");
|
|
57
|
+
for (const line of calls)
|
|
58
|
+
lines.push(line);
|
|
59
|
+
}
|
|
60
|
+
lines.push(formatUsage(summarizeUsage(events)));
|
|
61
|
+
lines.push(nextActor(work));
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
/** One line per tool call, in log order, with its outcome when it was rejected or failed. */
|
|
65
|
+
function toolLines(events) {
|
|
66
|
+
const lines = new Map();
|
|
67
|
+
for (const event of events) {
|
|
68
|
+
if (event.type === "tool.called") {
|
|
69
|
+
const { callId, name, input } = event.payload;
|
|
70
|
+
lines.set(callId, ` ${name} ${describeInput(input)}`.trimEnd());
|
|
71
|
+
}
|
|
72
|
+
else if (event.type === "tool.rejected") {
|
|
73
|
+
const { callId, name, code } = event.payload;
|
|
74
|
+
lines.set(callId, `${lines.get(callId) ?? ` ${name}`} 拒否(${rejectionLabel(code)})`);
|
|
75
|
+
}
|
|
76
|
+
else if (event.type === "tool.completed") {
|
|
77
|
+
const { callId, isError } = event.payload;
|
|
78
|
+
if (isError)
|
|
79
|
+
lines.set(callId, `${lines.get(callId) ?? ` ${callId}`} 失敗`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [...lines.values()];
|
|
83
|
+
}
|
|
84
|
+
function shorten(text) {
|
|
85
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
86
|
+
return oneLine.length > 40 ? `${oneLine.slice(0, 39)}…` : oneLine;
|
|
87
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Display width in a terminal: East Asian wide and full-width characters take two columns. */
|
|
2
|
+
export declare function displayWidth(text: string): number;
|
|
3
|
+
/** Pads with spaces to a display width, so columns line up with Japanese text in them. */
|
|
4
|
+
export declare function padDisplay(text: string, width: number): string;
|
|
5
|
+
/** A tool input on one line: the path when there is one, nothing for a question, otherwise the JSON, shortened. */
|
|
6
|
+
export declare function describeInput(input: unknown): string;
|
|
7
|
+
export declare function truncate(text: string, max?: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Text as it may reach a terminal: escape sequences and other control characters are dropped,
|
|
10
|
+
* newline and tab stay. Nothing a model says or a file contains can then move the cursor,
|
|
11
|
+
* retitle the window or write the clipboard. Invisible formatting characters (zero-width and
|
|
12
|
+
* bidirectional controls) go too, so a line cannot be made to read differently from what it is.
|
|
13
|
+
* A scan over the characters, not a regular expression, so the time is linear in the text.
|
|
14
|
+
*/
|
|
15
|
+
export declare function plain(text: string): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/** Display width in a terminal: East Asian wide and full-width characters take two columns. */
|
|
2
|
+
export function displayWidth(text) {
|
|
3
|
+
let width = 0;
|
|
4
|
+
for (const ch of text)
|
|
5
|
+
width += isWide(ch.codePointAt(0) ?? 0) ? 2 : 1;
|
|
6
|
+
return width;
|
|
7
|
+
}
|
|
8
|
+
function isWide(code) {
|
|
9
|
+
return ((code >= 0x1100 && code <= 0x115f) ||
|
|
10
|
+
(code >= 0x2e80 && code <= 0xa4cf) ||
|
|
11
|
+
(code >= 0xac00 && code <= 0xd7a3) ||
|
|
12
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
13
|
+
(code >= 0xfe30 && code <= 0xfe4f) ||
|
|
14
|
+
(code >= 0xff00 && code <= 0xff60) ||
|
|
15
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
16
|
+
(code >= 0x20000 && code <= 0x3fffd));
|
|
17
|
+
}
|
|
18
|
+
/** Pads with spaces to a display width, so columns line up with Japanese text in them. */
|
|
19
|
+
export function padDisplay(text, width) {
|
|
20
|
+
const missing = width - displayWidth(text);
|
|
21
|
+
return missing > 0 ? text + " ".repeat(missing) : text;
|
|
22
|
+
}
|
|
23
|
+
/** A tool input on one line: the path when there is one, nothing for a question, otherwise the JSON, shortened. */
|
|
24
|
+
export function describeInput(input) {
|
|
25
|
+
if (input && typeof input === "object") {
|
|
26
|
+
if ("path" in input)
|
|
27
|
+
return String(input.path);
|
|
28
|
+
if ("question" in input)
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
return truncate(JSON.stringify(input) ?? "");
|
|
32
|
+
}
|
|
33
|
+
export function truncate(text, max = 80) {
|
|
34
|
+
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Text as it may reach a terminal: escape sequences and other control characters are dropped,
|
|
38
|
+
* newline and tab stay. Nothing a model says or a file contains can then move the cursor,
|
|
39
|
+
* retitle the window or write the clipboard. Invisible formatting characters (zero-width and
|
|
40
|
+
* bidirectional controls) go too, so a line cannot be made to read differently from what it is.
|
|
41
|
+
* A scan over the characters, not a regular expression, so the time is linear in the text.
|
|
42
|
+
*/
|
|
43
|
+
export function plain(text) {
|
|
44
|
+
const chars = [...text];
|
|
45
|
+
let out = "";
|
|
46
|
+
let i = 0;
|
|
47
|
+
while (i < chars.length) {
|
|
48
|
+
const ch = chars[i];
|
|
49
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
50
|
+
if (code === ESC || code === CSI)
|
|
51
|
+
i = afterSequence(chars, i);
|
|
52
|
+
else {
|
|
53
|
+
if (!isControl(code))
|
|
54
|
+
out += ch;
|
|
55
|
+
i += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
const ESC = 0x1b;
|
|
61
|
+
const CSI = 0x9b;
|
|
62
|
+
const BEL = 0x07;
|
|
63
|
+
const ST = 0x9c;
|
|
64
|
+
function isControl(code) {
|
|
65
|
+
return ((code < 0x20 && code !== 0x0a && code !== 0x09) ||
|
|
66
|
+
(code >= 0x7f && code <= 0x9f) ||
|
|
67
|
+
(code >= 0x200b && code <= 0x200f) ||
|
|
68
|
+
(code >= 0x202a && code <= 0x202e) ||
|
|
69
|
+
(code >= 0x2066 && code <= 0x2069) ||
|
|
70
|
+
code === 0xfeff);
|
|
71
|
+
}
|
|
72
|
+
/** The index after the escape sequence that starts at `start`. An unfinished one runs to the end. */
|
|
73
|
+
function afterSequence(chars, start) {
|
|
74
|
+
const at = (i) => chars[i]?.codePointAt(0) ?? -1;
|
|
75
|
+
const end = chars.length;
|
|
76
|
+
const opener = at(start) === CSI ? "[" : chars[start + 1];
|
|
77
|
+
let i = at(start) === CSI ? start + 1 : start + 2;
|
|
78
|
+
if (opener === "[") {
|
|
79
|
+
// CSI: parameter and intermediate bytes, then one final byte.
|
|
80
|
+
while (i < end && at(i) >= 0x20 && at(i) <= 0x3f)
|
|
81
|
+
i += 1;
|
|
82
|
+
return Math.min(i + 1, end);
|
|
83
|
+
}
|
|
84
|
+
if (opener === "]" || opener === "P" || opener === "X" || opener === "^" || opener === "_") {
|
|
85
|
+
// OSC, DCS, SOS, PM, APC: a string that ends with BEL or ST.
|
|
86
|
+
while (i < end) {
|
|
87
|
+
if (at(i) === BEL || at(i) === ST)
|
|
88
|
+
return i + 1;
|
|
89
|
+
if (at(i) === ESC && chars[i + 1] === "\\")
|
|
90
|
+
return i + 2;
|
|
91
|
+
i += 1;
|
|
92
|
+
}
|
|
93
|
+
return end;
|
|
94
|
+
}
|
|
95
|
+
if (at(start + 1) >= 0x20 && at(start + 1) <= 0x2f) {
|
|
96
|
+
// Intermediate bytes, then one final byte.
|
|
97
|
+
while (i < end && at(i) >= 0x20 && at(i) <= 0x2f)
|
|
98
|
+
i += 1;
|
|
99
|
+
return Math.min(i + 1, end);
|
|
100
|
+
}
|
|
101
|
+
// ESC and one final character.
|
|
102
|
+
return Math.min(i, end);
|
|
103
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { AGENTS_TEMPLATE, CLAUDE_TEMPLATE, CONFIG_TEMPLATE, type InitOptions, init, MCP_TEMPLATE, } from "./commands/init.ts";
|
|
2
|
+
export { type McpOptions, mcp } from "./commands/mcp.ts";
|
|
3
|
+
export { type ToolsListOptions, toolsList } from "./commands/tools.ts";
|
|
4
|
+
export { describeWork, type WorkListOptions, type WorkShowOptions, workList, workShow, } from "./commands/work.ts";
|
|
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
|
+
export { formatUsage, summarizeUsage, type UsageSummary } from "./usage.ts";
|
|
8
|
+
export { findWorkspace } from "./workspace.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// openshain: Reference CLI agent for the openshain runtime
|
|
2
|
+
export { AGENTS_TEMPLATE, CLAUDE_TEMPLATE, CONFIG_TEMPLATE, init, MCP_TEMPLATE, } from "./commands/init.js";
|
|
3
|
+
export { mcp } from "./commands/mcp.js";
|
|
4
|
+
export { toolsList } from "./commands/tools.js";
|
|
5
|
+
export { describeWork, workList, workShow, } from "./commands/work.js";
|
|
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
|
+
export { formatUsage, summarizeUsage } from "./usage.js";
|
|
9
|
+
export { findWorkspace } from "./workspace.js";
|
package/dist/labels.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ErrorCode, FailureReason, ToolRejectionCode, WorkStatus } from "@openshain/core";
|
|
2
|
+
/** The words shown for a work's status. The log keeps the original value. */
|
|
3
|
+
export declare const STATUS_LABELS: Record<WorkStatus, string>;
|
|
4
|
+
/** Why a work failed, as a heading before the original detail. */
|
|
5
|
+
export declare const FAILURE_LABELS: Record<FailureReason, string>;
|
|
6
|
+
/** Why a tool call was rejected, as a heading before the original reason. */
|
|
7
|
+
export declare const REJECTION_LABELS: Record<ToolRejectionCode, string>;
|
|
8
|
+
/** A heading for a runtime error, before the original message. */
|
|
9
|
+
export declare const ERROR_LABELS: Record<ErrorCode, string>;
|
|
10
|
+
export declare function statusLabel(status: string): string;
|
|
11
|
+
export declare function failureLabel(reason: string | undefined): string;
|
|
12
|
+
export declare function rejectionLabel(code: string): string;
|
|
13
|
+
export declare function errorLabel(code: string): string | undefined;
|
package/dist/labels.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** The words shown for a work's status. The log keeps the original value. */
|
|
2
|
+
export const STATUS_LABELS = {
|
|
3
|
+
queued: "未着手",
|
|
4
|
+
in_progress: "進行中",
|
|
5
|
+
waiting_input: "利用者の入力待ち",
|
|
6
|
+
waiting_approval: "承認待ち",
|
|
7
|
+
waiting_external: "外部の応答待ち",
|
|
8
|
+
completed: "完了",
|
|
9
|
+
failed: "失敗",
|
|
10
|
+
cancelled: "取り消し",
|
|
11
|
+
};
|
|
12
|
+
/** Why a work failed, as a heading before the original detail. */
|
|
13
|
+
export const FAILURE_LABELS = {
|
|
14
|
+
limit_reached: "上限到達",
|
|
15
|
+
model_refusal: "model の拒否",
|
|
16
|
+
model_error: "model のエラー",
|
|
17
|
+
};
|
|
18
|
+
/** Why a tool call was rejected, as a heading before the original reason. */
|
|
19
|
+
export const REJECTION_LABELS = {
|
|
20
|
+
schema_mismatch: "schema に合わない入力",
|
|
21
|
+
unknown_tool: "知らない Tool",
|
|
22
|
+
not_allowed: "この workspace では不許可",
|
|
23
|
+
reserved_path: "予約されたパス",
|
|
24
|
+
outside_workspace: "workspace の外",
|
|
25
|
+
invalid_path: "不正なパス",
|
|
26
|
+
limit_reached: "Tool 呼び出しの上限",
|
|
27
|
+
};
|
|
28
|
+
/** A heading for a runtime error, before the original message. */
|
|
29
|
+
export const ERROR_LABELS = {
|
|
30
|
+
auth: "認証の失敗",
|
|
31
|
+
network: "接続の失敗",
|
|
32
|
+
rate_limit: "呼び出し上限",
|
|
33
|
+
invalid_response: "解釈できない model の応答",
|
|
34
|
+
config: "設定の問題",
|
|
35
|
+
corrupt_log: "壊れた Work の記録",
|
|
36
|
+
invalid_transition: "進められない状態",
|
|
37
|
+
duplicate_tool: "同じ名前の Tool の重複",
|
|
38
|
+
invalid_id: "不正な id",
|
|
39
|
+
invalid_tool: "不正な Tool の定義",
|
|
40
|
+
invalid_path: "不正なパス",
|
|
41
|
+
lock_held: "別のプロセスが使用中",
|
|
42
|
+
not_found: "対象なし",
|
|
43
|
+
reserved_path: "予約されたパス",
|
|
44
|
+
outside_workspace: "workspace の外",
|
|
45
|
+
concurrent_write: "同時書き込み",
|
|
46
|
+
invalid_event: "記録できないイベント",
|
|
47
|
+
};
|
|
48
|
+
export function statusLabel(status) {
|
|
49
|
+
return STATUS_LABELS[status] ?? status;
|
|
50
|
+
}
|
|
51
|
+
export function failureLabel(reason) {
|
|
52
|
+
return reason ? (FAILURE_LABELS[reason] ?? reason) : "理由は不明";
|
|
53
|
+
}
|
|
54
|
+
export function rejectionLabel(code) {
|
|
55
|
+
return REJECTION_LABELS[code] ?? code;
|
|
56
|
+
}
|
|
57
|
+
export function errorLabel(code) {
|
|
58
|
+
return ERROR_LABELS[code];
|
|
59
|
+
}
|
package/dist/report.d.ts
ADDED
|
@@ -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;
|