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.
- package/NOTICE +4 -0
- package/dist/bin.js +4 -33
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.js +7 -6
- package/dist/commands/tools.js +1 -2
- package/dist/commands/work.d.ts +1 -9
- package/dist/commands/work.js +9 -22
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/labels.d.ts +1 -2
- package/dist/labels.js +3 -0
- package/dist/preview.d.ts +13 -0
- package/dist/preview.js +141 -0
- package/dist/report.d.ts +6 -0
- package/dist/{commands/run.js → report.js} +6 -38
- package/dist/tui/app.js +38 -5
- package/dist/tui/banner.d.ts +3 -8
- package/dist/tui/banner.js +2 -2
- package/dist/tui/controller.d.ts +30 -5
- package/dist/tui/controller.js +306 -77
- package/dist/tui/index.js +1 -1
- package/dist/tui/lines.d.ts +5 -3
- package/dist/tui/lines.js +31 -3
- package/dist/tui/markdown.d.ts +15 -0
- package/dist/tui/markdown.js +199 -0
- package/dist/usage.d.ts +1 -1
- package/dist/usage.js +1 -1
- package/dist/workspace.js +1 -1
- package/package.json +9 -7
- package/src/bin.ts +4 -38
- package/src/commands/init.ts +7 -6
- package/src/commands/tools.ts +7 -2
- package/src/commands/work.ts +10 -34
- package/src/index.ts +1 -10
- package/src/labels.ts +4 -2
- package/src/preview.ts +157 -0
- package/src/{commands/run.ts → report.ts} +6 -59
- package/src/tui/app.tsx +75 -13
- package/src/tui/banner.ts +4 -10
- package/src/tui/controller.ts +338 -77
- package/src/tui/index.ts +3 -1
- package/src/tui/lines.ts +36 -6
- package/src/tui/markdown.ts +214 -0
- package/src/usage.ts +1 -2
- package/src/workspace.ts +1 -1
- package/dist/commands/run.d.ts +0 -22
package/dist/tui/lines.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { displayWidth } from "../format.js";
|
|
2
2
|
import { logoSegments } from "./banner.js";
|
|
3
|
+
import { markdownRows } from "./markdown.js";
|
|
3
4
|
/** What starts a line of each kind. The continuation lines of a wrapped entry are indented to match. */
|
|
4
5
|
const MARKERS = {
|
|
5
6
|
user: "> ",
|
|
@@ -31,6 +32,19 @@ export function wrapText(text, width) {
|
|
|
31
32
|
}
|
|
32
33
|
return out;
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The rows of one reply, kept until the entry goes or the width changes. The screen redraws
|
|
37
|
+
* every entry whenever a line is added, and reading markdown is the expensive part of that.
|
|
38
|
+
*/
|
|
39
|
+
const drawn = new WeakMap();
|
|
40
|
+
export function rowsFor(entry, width) {
|
|
41
|
+
const held = drawn.get(entry);
|
|
42
|
+
if (held && held.width === width)
|
|
43
|
+
return held.rows;
|
|
44
|
+
const rows = markdownRows(entry.text, width);
|
|
45
|
+
drawn.set(entry, { width, rows });
|
|
46
|
+
return rows;
|
|
47
|
+
}
|
|
34
48
|
/** A blank row goes before an entry that starts something new: a message, a reply, a notice, a question. */
|
|
35
49
|
function startsBlock(kind, previous) {
|
|
36
50
|
if (previous === undefined)
|
|
@@ -52,14 +66,28 @@ export function screenLines(entries, width) {
|
|
|
52
66
|
lines.push({ kind: "blank", text: "" });
|
|
53
67
|
if (entry.kind === "logo") {
|
|
54
68
|
// Never wrapped: a cut row of the wordmark reads better than a broken one.
|
|
55
|
-
lines.push({ kind: "logo", text: entry.text,
|
|
69
|
+
lines.push({ kind: "logo", text: entry.text, spans: logoSegments(entry.text) });
|
|
56
70
|
previous = entry.kind;
|
|
57
71
|
continue;
|
|
58
72
|
}
|
|
59
73
|
const marker = MARKERS[entry.kind];
|
|
60
74
|
const indent = " ".repeat(displayWidth(marker));
|
|
61
|
-
const
|
|
62
|
-
|
|
75
|
+
const room = Math.max(8, width - displayWidth(marker));
|
|
76
|
+
if (entry.kind === "assistant") {
|
|
77
|
+
// The reply is written in markdown; the screen draws it rather than showing its marks.
|
|
78
|
+
for (const [i, row] of rowsFor(entry, room).entries()) {
|
|
79
|
+
// A row with nothing on it is drawn as an empty one: no marker, no indent, no pieces.
|
|
80
|
+
if (row.length === 0) {
|
|
81
|
+
lines.push({ kind: entry.kind, text: "" });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const spans = [{ text: i === 0 ? marker : indent }, ...row];
|
|
85
|
+
lines.push({ kind: entry.kind, text: spans.map((s) => s.text).join(""), spans });
|
|
86
|
+
}
|
|
87
|
+
previous = entry.kind;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
for (const [i, text] of wrapText(entry.text, room).entries()) {
|
|
63
91
|
lines.push({ kind: entry.kind, text: (i === 0 ? marker : indent) + text });
|
|
64
92
|
}
|
|
65
93
|
previous = entry.kind;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A piece of a row that carries one style. A row is a list of these, drawn left to right. */
|
|
2
|
+
export interface Span {
|
|
3
|
+
text: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
bold?: boolean;
|
|
6
|
+
italic?: boolean;
|
|
7
|
+
dim?: boolean;
|
|
8
|
+
strikethrough?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A reply as rows of styled pieces. The model writes markdown, so the screen shows the emphasis
|
|
12
|
+
* and the structure instead of the characters that mark them. What this does not draw yet is
|
|
13
|
+
* shown as it was written, never dropped.
|
|
14
|
+
*/
|
|
15
|
+
export declare function markdownRows(source: string, width: number): Span[][];
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import { displayWidth } from "../format.js";
|
|
3
|
+
/** What each part of a reply looks like on the screen. */
|
|
4
|
+
const STYLE = {
|
|
5
|
+
heading: { bold: true, color: "cyan" },
|
|
6
|
+
code: { color: "green" },
|
|
7
|
+
link: { color: "blue" },
|
|
8
|
+
quote: { dim: true },
|
|
9
|
+
rule: { dim: true },
|
|
10
|
+
};
|
|
11
|
+
const QUOTE_MARKER = "▎ ";
|
|
12
|
+
const CODE_MARKER = "│ ";
|
|
13
|
+
const BULLETS = ["•", "◦", "‣"];
|
|
14
|
+
function styled(text, style) {
|
|
15
|
+
return { text, ...style };
|
|
16
|
+
}
|
|
17
|
+
/** The inline tokens of one block, flattened into styled pieces. */
|
|
18
|
+
function inline(tokens, style) {
|
|
19
|
+
if (!tokens)
|
|
20
|
+
return [];
|
|
21
|
+
const spans = [];
|
|
22
|
+
for (const token of tokens) {
|
|
23
|
+
switch (token.type) {
|
|
24
|
+
case "strong":
|
|
25
|
+
spans.push(...inline(token.tokens, { ...style, bold: true }));
|
|
26
|
+
break;
|
|
27
|
+
case "em":
|
|
28
|
+
spans.push(...inline(token.tokens, { ...style, italic: true }));
|
|
29
|
+
break;
|
|
30
|
+
case "del":
|
|
31
|
+
spans.push(...inline(token.tokens, { ...style, strikethrough: true }));
|
|
32
|
+
break;
|
|
33
|
+
case "codespan":
|
|
34
|
+
spans.push(styled(token.text, { ...style, ...STYLE.code }));
|
|
35
|
+
break;
|
|
36
|
+
case "link": {
|
|
37
|
+
const link = token;
|
|
38
|
+
spans.push(...inline(link.tokens, style));
|
|
39
|
+
// The label alone hides where the link goes, so the address follows it.
|
|
40
|
+
if (link.href && link.href !== textOf(link.tokens)) {
|
|
41
|
+
spans.push(styled(` (${link.href})`, { ...style, ...STYLE.link }));
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
case "br":
|
|
46
|
+
spans.push(styled("\n", style));
|
|
47
|
+
break;
|
|
48
|
+
case "escape":
|
|
49
|
+
case "text":
|
|
50
|
+
spans.push(...(token.tokens
|
|
51
|
+
? inline(token.tokens, style)
|
|
52
|
+
: [styled(token.text, style)]));
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
spans.push(styled(token.raw, style));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return spans;
|
|
59
|
+
}
|
|
60
|
+
function textOf(tokens) {
|
|
61
|
+
return inline(tokens, {})
|
|
62
|
+
.map((s) => s.text)
|
|
63
|
+
.join("");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Breaks styled pieces into rows no wider than `width` display columns, the way the rest of the
|
|
67
|
+
* screen breaks plain text: at the character, so Japanese wraps where it should. Rows after the
|
|
68
|
+
* first start with `hanging`, which keeps a list item under its own marker.
|
|
69
|
+
*/
|
|
70
|
+
function wrap(spans, width, hanging = "") {
|
|
71
|
+
const limit = Math.max(4, width);
|
|
72
|
+
const rows = [];
|
|
73
|
+
let row = [];
|
|
74
|
+
let used = 0;
|
|
75
|
+
const indent = () => (hanging === "" ? [] : [{ text: hanging }]);
|
|
76
|
+
const start = () => {
|
|
77
|
+
rows.push(row);
|
|
78
|
+
row = indent();
|
|
79
|
+
used = displayWidth(hanging);
|
|
80
|
+
};
|
|
81
|
+
for (const span of spans) {
|
|
82
|
+
for (const [i, part] of span.text.split("\n").entries()) {
|
|
83
|
+
// A line break inside a block starts a row of its own.
|
|
84
|
+
if (i > 0)
|
|
85
|
+
start();
|
|
86
|
+
let piece = "";
|
|
87
|
+
for (const ch of part) {
|
|
88
|
+
const w = displayWidth(ch);
|
|
89
|
+
if (used + w > limit && (row.length > 0 || piece !== "")) {
|
|
90
|
+
if (piece !== "")
|
|
91
|
+
row.push({ ...span, text: piece });
|
|
92
|
+
piece = "";
|
|
93
|
+
start();
|
|
94
|
+
}
|
|
95
|
+
piece += ch;
|
|
96
|
+
used += w;
|
|
97
|
+
}
|
|
98
|
+
if (piece !== "")
|
|
99
|
+
row.push({ ...span, text: piece });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
rows.push(row);
|
|
103
|
+
return rows;
|
|
104
|
+
}
|
|
105
|
+
/** Puts `prefix` in front of every row, for a quote bar or a code bar. */
|
|
106
|
+
function prefixed(rows, prefix) {
|
|
107
|
+
return rows.map((row) => [prefix, ...row]);
|
|
108
|
+
}
|
|
109
|
+
function blockRows(tokens, width) {
|
|
110
|
+
const rows = [];
|
|
111
|
+
for (const token of tokens) {
|
|
112
|
+
switch (token.type) {
|
|
113
|
+
case "space":
|
|
114
|
+
rows.push([]);
|
|
115
|
+
break;
|
|
116
|
+
case "heading":
|
|
117
|
+
rows.push(...wrap(inline(token.tokens, STYLE.heading), width));
|
|
118
|
+
break;
|
|
119
|
+
case "paragraph":
|
|
120
|
+
case "text":
|
|
121
|
+
rows.push(...wrap(inline(token.tokens ?? [], {}), width));
|
|
122
|
+
break;
|
|
123
|
+
case "code": {
|
|
124
|
+
const marker = styled(CODE_MARKER, STYLE.quote);
|
|
125
|
+
const body = token.text.split("\n");
|
|
126
|
+
for (const line of body) {
|
|
127
|
+
rows.push(...prefixed(wrap([styled(line, STYLE.code)], width - 2), marker));
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
case "blockquote": {
|
|
132
|
+
const inner = blockRows(token.tokens ?? [], width - 2);
|
|
133
|
+
rows.push(...prefixed(inner, styled(QUOTE_MARKER, STYLE.quote)));
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "list":
|
|
137
|
+
rows.push(...listRows(token, width, 0));
|
|
138
|
+
break;
|
|
139
|
+
case "hr":
|
|
140
|
+
rows.push([styled("─".repeat(Math.max(4, width)), STYLE.rule)]);
|
|
141
|
+
break;
|
|
142
|
+
case "table":
|
|
143
|
+
// Aligning columns is its own piece of work; until then the source rows are shown as
|
|
144
|
+
// they were written, so nothing the model put in the table is lost.
|
|
145
|
+
for (const line of token.raw.trimEnd().split("\n")) {
|
|
146
|
+
rows.push(...wrap([{ text: line }], width));
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
default:
|
|
150
|
+
for (const line of (token.raw ?? "").trimEnd().split("\n")) {
|
|
151
|
+
rows.push(...wrap([{ text: line }], width));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return rows;
|
|
156
|
+
}
|
|
157
|
+
function listRows(list, width, depth) {
|
|
158
|
+
const rows = [];
|
|
159
|
+
let number = Number(list.start || 1);
|
|
160
|
+
for (const item of list.items) {
|
|
161
|
+
const marker = list.ordered ? `${number++}. ` : `${BULLETS[depth % BULLETS.length]} `;
|
|
162
|
+
const indent = " ".repeat(displayWidth(marker));
|
|
163
|
+
const inner = [];
|
|
164
|
+
for (const token of item.tokens) {
|
|
165
|
+
if (token.type === "list") {
|
|
166
|
+
inner.push(...listRows(token, width - displayWidth(marker), depth + 1));
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
inner.push(...blockRows([token], width - displayWidth(marker)));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const [i, row] of inner.entries()) {
|
|
173
|
+
rows.push([{ text: i === 0 ? marker : indent }, ...row]);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return rows;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* How much of a reply is read as markdown. Reading it costs more than the square of its length
|
|
180
|
+
* (10,000 characters take about 0.14 seconds, 20,000 about 0.46, 140,000 over a minute), and the
|
|
181
|
+
* screen draws on one thread, so a longer reply would hold it. Above this the reply is shown as
|
|
182
|
+
* plain text: every character is still there, with its marks.
|
|
183
|
+
*/
|
|
184
|
+
const MAX_SOURCE = 20_000;
|
|
185
|
+
/**
|
|
186
|
+
* A reply as rows of styled pieces. The model writes markdown, so the screen shows the emphasis
|
|
187
|
+
* and the structure instead of the characters that mark them. What this does not draw yet is
|
|
188
|
+
* shown as it was written, never dropped.
|
|
189
|
+
*/
|
|
190
|
+
export function markdownRows(source, width) {
|
|
191
|
+
if (source.length > MAX_SOURCE)
|
|
192
|
+
return wrap([{ text: source }], width);
|
|
193
|
+
const rows = blockRows(marked.lexer(source), width);
|
|
194
|
+
while (rows.length > 0 && (rows[0]?.length ?? 0) === 0)
|
|
195
|
+
rows.shift();
|
|
196
|
+
while (rows.length > 0 && (rows.at(-1)?.length ?? 0) === 0)
|
|
197
|
+
rows.pop();
|
|
198
|
+
return rows.length > 0 ? rows : [[]];
|
|
199
|
+
}
|
package/dist/usage.d.ts
CHANGED
package/dist/usage.js
CHANGED
package/dist/workspace.js
CHANGED
|
@@ -12,7 +12,7 @@ export async function findWorkspace(start) {
|
|
|
12
12
|
catch {
|
|
13
13
|
const parent = dirname(dir);
|
|
14
14
|
if (parent === dir) {
|
|
15
|
-
throw new OpenshainError("config", `${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init
|
|
15
|
+
throw new OpenshainError("config", `${CONFIG_FILE_NAME} が見つかりません。${resolve(start)} から上に向かって探しました。openshain init で作成します。`);
|
|
16
16
|
}
|
|
17
17
|
dir = parent;
|
|
18
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openshain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Reference CLI of the openshain agent harness",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openshain",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"!src/**/*.test.ts",
|
|
31
31
|
"!src/**/*.test.tsx",
|
|
32
32
|
"README.md",
|
|
33
|
-
"LICENSE"
|
|
33
|
+
"LICENSE",
|
|
34
|
+
"NOTICE"
|
|
34
35
|
],
|
|
35
36
|
"exports": {
|
|
36
37
|
".": {
|
|
@@ -40,7 +41,7 @@
|
|
|
40
41
|
}
|
|
41
42
|
},
|
|
42
43
|
"bin": {
|
|
43
|
-
"openshain": "
|
|
44
|
+
"openshain": "dist/bin.js"
|
|
44
45
|
},
|
|
45
46
|
"scripts": {
|
|
46
47
|
"build": "../../node_modules/.bin/tsc -p tsconfig.build.json",
|
|
@@ -48,11 +49,12 @@
|
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
51
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
51
|
-
"@openshain/agent": "0.
|
|
52
|
-
"@openshain/core": "0.
|
|
53
|
-
"@openshain/mcp": "0.
|
|
54
|
-
"@openshain/tools": "0.
|
|
52
|
+
"@openshain/agent": "0.4.0",
|
|
53
|
+
"@openshain/core": "0.4.0",
|
|
54
|
+
"@openshain/mcp": "0.4.0",
|
|
55
|
+
"@openshain/tools": "0.4.0",
|
|
55
56
|
"ink": "7.1.1",
|
|
57
|
+
"marked": "18.0.12",
|
|
56
58
|
"react": "19.2.8"
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
package/src/bin.ts
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, type RuntimeProviders } from "@openshain/core";
|
|
6
5
|
import { standardTools } from "@openshain/tools";
|
|
7
6
|
import { init } from "./commands/init.ts";
|
|
8
7
|
import { mcp } from "./commands/mcp.ts";
|
|
9
|
-
import { run } from "./commands/run.ts";
|
|
10
8
|
import { toolsList } from "./commands/tools.ts";
|
|
11
|
-
import { workList,
|
|
9
|
+
import { workList, workShow } from "./commands/work.ts";
|
|
12
10
|
import { plain } from "./format.ts";
|
|
13
11
|
import { errorLabel } from "./labels.ts";
|
|
14
12
|
import { startTui } from "./tui/index.ts";
|
|
@@ -17,11 +15,9 @@ import { findWorkspace } from "./workspace.ts";
|
|
|
17
15
|
const USAGE = `使い方:
|
|
18
16
|
openshain 端末で対話を始める
|
|
19
17
|
openshain init openshain.yaml のひな型を書く
|
|
20
|
-
openshain run "<依頼>" 依頼を Work として進める
|
|
21
18
|
openshain tools list 使える Tool の一覧
|
|
22
19
|
openshain work list Work の一覧
|
|
23
20
|
openshain work show <id> Work の詳細
|
|
24
|
-
openshain work resume <id> 途中で止まった Work を続ける
|
|
25
21
|
openshain mcp MCP Server を stdio で起動する
|
|
26
22
|
|
|
27
23
|
--workspace <dir> 起点のディレクトリ。省略時はカレントディレクトリ
|
|
@@ -67,17 +63,6 @@ async function main(argv: string[]): Promise<number> {
|
|
|
67
63
|
case "init":
|
|
68
64
|
await init({ workspaceRoot: values.workspace ?? process.cwd(), write });
|
|
69
65
|
return 0;
|
|
70
|
-
case "run": {
|
|
71
|
-
const objective = rest.join(" ").trim();
|
|
72
|
-
if (!objective) {
|
|
73
|
-
write('依頼の文を指定してください。openshain run "今月の経理を進めて" のように。');
|
|
74
|
-
return 2;
|
|
75
|
-
}
|
|
76
|
-
const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
|
|
77
|
-
return withTerminal((ask) =>
|
|
78
|
-
run({ workspaceRoot, providers, objective, write, ...(ask && { ask }) }),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
66
|
case "mcp": {
|
|
82
67
|
const workspaceRoot = await findWorkspace(values.workspace ?? process.cwd());
|
|
83
68
|
await mcp({ workspaceRoot, providers });
|
|
@@ -95,7 +80,7 @@ async function main(argv: string[]): Promise<number> {
|
|
|
95
80
|
case "work": {
|
|
96
81
|
const sub = rest[0];
|
|
97
82
|
const id = rest[1] ?? "";
|
|
98
|
-
if (!(sub === "list" || (
|
|
83
|
+
if (!(sub === "list" || (sub === "show" && id))) {
|
|
99
84
|
write(USAGE);
|
|
100
85
|
return 2;
|
|
101
86
|
}
|
|
@@ -104,13 +89,8 @@ async function main(argv: string[]): Promise<number> {
|
|
|
104
89
|
await workList({ workspaceRoot, write });
|
|
105
90
|
return 0;
|
|
106
91
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return 0;
|
|
110
|
-
}
|
|
111
|
-
return withTerminal((ask) =>
|
|
112
|
-
workResume({ workspaceRoot, providers, id, write, ...(ask && { ask }) }),
|
|
113
|
-
);
|
|
92
|
+
await workShow({ workspaceRoot, id, write });
|
|
93
|
+
return 0;
|
|
114
94
|
}
|
|
115
95
|
default:
|
|
116
96
|
write(`不明なコマンド ${command}`);
|
|
@@ -119,20 +99,6 @@ async function main(argv: string[]): Promise<number> {
|
|
|
119
99
|
}
|
|
120
100
|
}
|
|
121
101
|
|
|
122
|
-
/** Runs `fn` with a way to ask the person when a terminal is there to answer; otherwise without one. */
|
|
123
|
-
async function withTerminal(
|
|
124
|
-
fn: (ask?: (question: string) => Promise<string>) => Promise<number>,
|
|
125
|
-
): Promise<number> {
|
|
126
|
-
// Without a terminal there is no one to answer; the work waits instead.
|
|
127
|
-
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) return fn();
|
|
128
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
129
|
-
try {
|
|
130
|
-
return await fn((question) => rl.question(`${plain(question)}\n> `));
|
|
131
|
-
} finally {
|
|
132
|
-
rl.close();
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
102
|
main(process.argv.slice(2)).then(
|
|
137
103
|
(code) => process.exit(code),
|
|
138
104
|
(err: unknown) => {
|
package/src/commands/init.ts
CHANGED
|
@@ -21,7 +21,7 @@ profession:
|
|
|
21
21
|
id: generic
|
|
22
22
|
# model への指示。100,000 文字まで
|
|
23
23
|
instructions: |
|
|
24
|
-
|
|
24
|
+
あなたはこの会社の一般事務の社員エージェントです。依頼された作業を、workspace 内のファイルだけを使って進めてください。
|
|
25
25
|
model:
|
|
26
26
|
provider: anthropic # anthropic | openai-compatible
|
|
27
27
|
model: claude-opus-5
|
|
@@ -52,7 +52,7 @@ export const MCP_TEMPLATE = `${JSON.stringify(
|
|
|
52
52
|
/** What an outside agent reads before working in the folder. Codex reads AGENTS.md; Claude Code reads it through CLAUDE.md. */
|
|
53
53
|
export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェントへ
|
|
54
54
|
|
|
55
|
-
このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP
|
|
55
|
+
このフォルダは openshain の Company Workspace です。この指示は、Claude Code や Codex のような外部のエージェントが MCP 経由でこのフォルダを扱うときのものです。openshain の対話型 CLI も同じ手順で Runtime を使います。
|
|
56
56
|
|
|
57
57
|
会社のファイルの読み書きと集計は openshain の MCP tool で行います。Claude Code や Codex 自身の Read、Write、Bash は会社のファイルには使いません。Runtime を通らなかった操作は記録に残らないためです。
|
|
58
58
|
|
|
@@ -60,7 +60,8 @@ export const AGENTS_TEMPLATE = `# この会社フォルダで働くエージェ
|
|
|
60
60
|
- ファイルは \`fs_list\`、\`fs_search\`、\`fs_read\`、\`csv_read\`、\`markdown_read\` で見る。合計や件数は \`csv_aggregate\` に任せ、自分で合計しない
|
|
61
61
|
- 書くときは \`fs_write\` か \`csv_write\`
|
|
62
62
|
- 終わったら \`work_complete\` に、何をしたかと、書いたファイルを渡す。続けられないときは \`work_fail\`
|
|
63
|
-
- \`openshain.yaml
|
|
63
|
+
- \`openshain.yaml\`、\`work/\`、\`principals/\`、\`authority/\` は Runtime のもの。変更しない
|
|
64
|
+
- 呼び出しの結果が \`pending\` なら、会社の権限の規則がその呼び出しを止めている。承認は会社の人が openshain の画面で決める。自分で \`approval_decide\` や \`review_decide\` を呼んで通さない。人に伝えて、決まるまで別の作業をする
|
|
64
65
|
`;
|
|
65
66
|
|
|
66
67
|
export const CLAUDE_TEMPLATE = "@AGENTS.md\n";
|
|
@@ -110,7 +111,7 @@ export async function init({ workspaceRoot, write }: InitOptions): Promise<void>
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
write(
|
|
113
|
-
"company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain
|
|
114
|
+
"company と principal を自分の会社に合わせ、api_key_env に書いた環境変数を設定してから openshain を実行してください。",
|
|
114
115
|
);
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -123,10 +124,10 @@ async function addToMcpConfig(path: string): Promise<string> {
|
|
|
123
124
|
try {
|
|
124
125
|
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
125
126
|
} catch {
|
|
126
|
-
return `${path} はすでにあり、JSON として読めないので変更しません。openshain
|
|
127
|
+
return `${path} はすでにあり、JSON として読めないので変更しません。openshain を手動で登録してください。`;
|
|
127
128
|
}
|
|
128
129
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
129
|
-
return `${path} はすでにあり、形が違うので変更しません。openshain
|
|
130
|
+
return `${path} はすでにあり、形が違うので変更しません。openshain を手動で登録してください。`;
|
|
130
131
|
}
|
|
131
132
|
const config = parsed as { mcpServers?: unknown };
|
|
132
133
|
const servers =
|
package/src/commands/tools.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ASK_USER,
|
|
3
|
+
createToolRegistry,
|
|
4
|
+
loadConfig,
|
|
5
|
+
RUNTIME_PROVIDER_ID,
|
|
6
|
+
type RuntimeProviders,
|
|
7
|
+
} from "@openshain/core";
|
|
3
8
|
|
|
4
9
|
export interface ToolsListOptions {
|
|
5
10
|
workspaceRoot: string;
|
package/src/commands/work.ts
CHANGED
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
import { pendingQuestions } from "@openshain/agent";
|
|
2
1
|
import {
|
|
3
2
|
type AnyEvent,
|
|
4
|
-
createRuntime,
|
|
5
3
|
type Event,
|
|
6
|
-
isTerminal,
|
|
7
4
|
parseWorkId,
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
pendingApprovals,
|
|
6
|
+
pendingQuestions,
|
|
10
7
|
type Work,
|
|
11
8
|
WorkStore,
|
|
12
9
|
} from "@openshain/core";
|
|
13
10
|
import { describeInput, padDisplay } from "../format.ts";
|
|
14
11
|
import { errorLabel, failureLabel, rejectionLabel, statusLabel } from "../labels.ts";
|
|
12
|
+
import { nextActor } from "../report.ts";
|
|
15
13
|
import { formatUsage, summarizeUsage } from "../usage.ts";
|
|
16
|
-
import { type DriveOptions, drive, nextActor } from "./run.ts";
|
|
17
14
|
|
|
18
15
|
export interface WorkListOptions {
|
|
19
16
|
workspaceRoot: string;
|
|
@@ -24,7 +21,7 @@ export interface WorkListOptions {
|
|
|
24
21
|
export async function workList({ workspaceRoot, write }: WorkListOptions): Promise<void> {
|
|
25
22
|
const { works, problems } = await new WorkStore(workspaceRoot).list();
|
|
26
23
|
if (works.length === 0 && problems.length === 0) {
|
|
27
|
-
write(
|
|
24
|
+
write("Work はまだありません。openshain で社員エージェントに依頼すると始まります。");
|
|
28
25
|
return;
|
|
29
26
|
}
|
|
30
27
|
for (const work of works) {
|
|
@@ -75,6 +72,12 @@ export function describeWork(work: Work, events: AnyEvent[]): string[] {
|
|
|
75
72
|
if (work.status === "waiting_input") {
|
|
76
73
|
for (const { question } of pendingQuestions(events)) lines.push(`質問 ${question}`);
|
|
77
74
|
}
|
|
75
|
+
if (work.status === "waiting_approval") {
|
|
76
|
+
for (const a of pendingApprovals(events)) {
|
|
77
|
+
const who = a.kind === "review" ? `${a.reviewer?.role ?? "資格者"}の判断待ち` : "承認待ち";
|
|
78
|
+
lines.push(`${who} ${a.call.name} ${describeInput(a.call.input)} (${a.approvalId})`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
78
81
|
|
|
79
82
|
const calls = toolLines(events);
|
|
80
83
|
if (calls.length > 0) {
|
|
@@ -86,33 +89,6 @@ export function describeWork(work: Work, events: AnyEvent[]): string[] {
|
|
|
86
89
|
return lines;
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
export interface WorkResumeOptions extends DriveOptions {
|
|
90
|
-
workspaceRoot: string;
|
|
91
|
-
providers: RuntimeProviders;
|
|
92
|
-
id: string;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Continues a work that stopped before its end, answering its questions when the caller can. */
|
|
96
|
-
export async function workResume(options: WorkResumeOptions): Promise<number> {
|
|
97
|
-
const workId = parseWorkId(options.id);
|
|
98
|
-
const work = await new WorkStore(options.workspaceRoot).get(workId);
|
|
99
|
-
if (work.type === SESSION_WORK_TYPE) {
|
|
100
|
-
options.write(
|
|
101
|
-
`${work.id} は会話の記録のため、再開できません。会話は openshain で始め直してください。`,
|
|
102
|
-
);
|
|
103
|
-
return 1;
|
|
104
|
-
}
|
|
105
|
-
if (isTerminal(work.status)) {
|
|
106
|
-
options.write(`${work.id} は${statusLabel(work.status)}のため、再開できません。`);
|
|
107
|
-
return 1;
|
|
108
|
-
}
|
|
109
|
-
const runtime = await createRuntime({
|
|
110
|
-
workspaceRoot: options.workspaceRoot,
|
|
111
|
-
providers: options.providers,
|
|
112
|
-
});
|
|
113
|
-
return drive(runtime, workId, options);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
92
|
/** One line per tool call, in log order, with its outcome when it was rejected or failed. */
|
|
117
93
|
function toolLines(events: AnyEvent[]): string[] {
|
|
118
94
|
const lines = new Map<string, string>();
|
package/src/index.ts
CHANGED
|
@@ -8,22 +8,12 @@ export {
|
|
|
8
8
|
MCP_TEMPLATE,
|
|
9
9
|
} from "./commands/init.ts";
|
|
10
10
|
export { type McpOptions, mcp } from "./commands/mcp.ts";
|
|
11
|
-
export {
|
|
12
|
-
type DriveOptions,
|
|
13
|
-
drive,
|
|
14
|
-
nextActor,
|
|
15
|
-
type RunOptions,
|
|
16
|
-
report,
|
|
17
|
-
run,
|
|
18
|
-
} from "./commands/run.ts";
|
|
19
11
|
export { type ToolsListOptions, toolsList } from "./commands/tools.ts";
|
|
20
12
|
export {
|
|
21
13
|
describeWork,
|
|
22
14
|
type WorkListOptions,
|
|
23
|
-
type WorkResumeOptions,
|
|
24
15
|
type WorkShowOptions,
|
|
25
16
|
workList,
|
|
26
|
-
workResume,
|
|
27
17
|
workShow,
|
|
28
18
|
} from "./commands/work.ts";
|
|
29
19
|
export {
|
|
@@ -36,5 +26,6 @@ export {
|
|
|
36
26
|
STATUS_LABELS,
|
|
37
27
|
statusLabel,
|
|
38
28
|
} from "./labels.ts";
|
|
29
|
+
export { nextActor, progressLine, report } from "./report.ts";
|
|
39
30
|
export { formatUsage, summarizeUsage, type UsageSummary } from "./usage.ts";
|
|
40
31
|
export { findWorkspace } from "./workspace.ts";
|
package/src/labels.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { FailureReason } from "@openshain/
|
|
2
|
-
import type { ErrorCode, ToolRejectionCode, WorkStatus } from "@openshain/core";
|
|
1
|
+
import type { ErrorCode, FailureReason, ToolRejectionCode, WorkStatus } from "@openshain/core";
|
|
3
2
|
|
|
4
3
|
/** The words shown for a work's status. The log keeps the original value. */
|
|
5
4
|
export const STATUS_LABELS: Record<WorkStatus, string> = {
|
|
@@ -28,6 +27,9 @@ export const REJECTION_LABELS: Record<ToolRejectionCode, string> = {
|
|
|
28
27
|
reserved_path: "予約されたパス",
|
|
29
28
|
outside_workspace: "workspace の外",
|
|
30
29
|
invalid_path: "不正なパス",
|
|
30
|
+
limit_reached: "Tool 呼び出しの上限",
|
|
31
|
+
denied: "権限の表で不許可",
|
|
32
|
+
rejected_by_person: "承認されなかった",
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
/** A heading for a runtime error, before the original message. */
|