@shimatoworks/stw-agent 0.2.0 → 0.3.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/README.md +34 -3
- package/bin/stw-agent.mjs +191 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,9 +35,33 @@ set -a && source ~/.config/tamai/stw-agent.env && set +a
|
|
|
35
35
|
stw-agent --help
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
動詞は `doctor` / `projects list` / `projects context` / `
|
|
39
|
-
`slack messages` / `slack next` の
|
|
40
|
-
|
|
38
|
+
動詞は `doctor` / `projects list` / `projects context` / `projects resources` /
|
|
39
|
+
`projects next` / `slack channels` / `slack messages` / `slack next` の 8 つ。
|
|
40
|
+
正確なオプションは常に `--help` を見る(ここに写経しない)。
|
|
41
|
+
|
|
42
|
+
### Drive の資料を探す(`projects resources`)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
stw-agent projects resources yoshino
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
案件に紐付いた **Drive 入口フォルダ配下の索引**(名前・MIME・更新日・親子・
|
|
49
|
+
ショートカット先・「資料候補」の印)を返す。入口の URL だけでは
|
|
50
|
+
「直下に 1 フォルダしかなく、主要資料はさらに下」という形に辿り着けないため。
|
|
51
|
+
|
|
52
|
+
**本文は返らない。** `fileId` を `gog` へ渡して読む(guide §4)。
|
|
53
|
+
|
|
54
|
+
読み方で気をつけること:
|
|
55
|
+
|
|
56
|
+
- 索引は「**SA が読めるメタデータを、台帳で指定された受け手へ公開する**」もので、
|
|
57
|
+
本人が Google で読める全ファイルではない。入口ごとに admin が有効化したものだけが出る
|
|
58
|
+
- `indexes[].complete` が false なら**まだ全部ではない**(`reason` に理由がある)。
|
|
59
|
+
**終了コード 0 でも起こる**ので、「これで全部」と書かない
|
|
60
|
+
- 続きは出力の「次:」行をそのまま実行する(`projects next '<query>' --slug <slug>`)。
|
|
61
|
+
cursor は不透明な値で、手で組み立てると 400 になる
|
|
62
|
+
- `409` は索引が作り直された合図。`--cursor` を外して取り直す
|
|
63
|
+
- `503 drive_unavailable` は一時的な確認失敗。**古い一覧を成功として返さない**ので、
|
|
64
|
+
空ではなくエラーになる
|
|
41
65
|
|
|
42
66
|
### まず `doctor`
|
|
43
67
|
|
|
@@ -73,6 +97,13 @@ token・読み取り API への到達・token の権限・周辺ツール(`ntn
|
|
|
73
97
|
|
|
74
98
|
## 変更履歴
|
|
75
99
|
|
|
100
|
+
- **0.3.0** — `projects resources`(Drive 入口配下の索引)と `projects next` を追加。
|
|
101
|
+
資料候補をページ内で先に出し、`complete=false` のときは理由を必ず添える。
|
|
102
|
+
409 / `503 drive_unavailable` に「取り直す」「一時的」の案内を付けた。
|
|
103
|
+
**本文は返さない**(`fileId` を `gog` へ渡す)
|
|
104
|
+
- **0.2.1** — `doctor` の bun の案内を「入れたあと PATH に `~/.bun/bin` を足す」まで書くようにした。
|
|
105
|
+
publish は GitHub Actions(npm trusted publishing・タグ push)へ移した
|
|
106
|
+
(repo が private なので provenance は付かない)
|
|
76
107
|
- **0.2.0** — `stw-agent doctor` を追加(token・API 到達・権限・周辺ツールの一括確認)。
|
|
77
108
|
`STW_API_BASE` に資格情報が入っていれば要求前に拒否し、表示は origin だけにする。
|
|
78
109
|
`slack channels` の既定出力に、サーバが返すようになれば保存範囲(最古〜最新・件数)と
|
package/bin/stw-agent.mjs
CHANGED
|
@@ -53,11 +53,14 @@ export const USAGE = `使い方:
|
|
|
53
53
|
stw-agent projects context <slug> [--include-slack <true|false>]
|
|
54
54
|
[--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>]
|
|
55
55
|
[--q <text>] [--limit <n>] [--json]
|
|
56
|
+
stw-agent projects resources <slug> [--system drive] [--resource-id <uuid>]
|
|
57
|
+
[--limit <n>] [--cursor <c>] [--json]
|
|
56
58
|
stw-agent slack channels [--project <slug>] [--limit <n>] [--cursor <c>] [--json]
|
|
57
59
|
stw-agent slack messages (--project <slug> | --channel <id>)
|
|
58
60
|
[--from <YYYY-MM-DD>] [--to <YYYY-MM-DD>] [--q <text>]
|
|
59
61
|
[--limit <n>] [--thread <ts>] [--cursor <c>] [--json]
|
|
60
62
|
stw-agent slack next <continuationQuery> [--json]
|
|
63
|
+
stw-agent projects next <continuationQuery> --slug <slug> [--json]
|
|
61
64
|
stw-agent doctor [--json]
|
|
62
65
|
|
|
63
66
|
環境変数:
|
|
@@ -72,6 +75,11 @@ export const USAGE = `使い方:
|
|
|
72
75
|
docs/guides/案件コンテキストの集め方.md を見る。
|
|
73
76
|
既定の出力は 1 行要約(本文は先頭だけ)。全文が要るときだけ --json を付ける。
|
|
74
77
|
期間を省略すると当日を含む直近 30 日。続きは continuationQuery を slack next へそのまま渡す。
|
|
78
|
+
|
|
79
|
+
projects resources は Drive 入口の**索引**(名前・MIME・更新日・親子・ショートカット先)を返す。
|
|
80
|
+
**本文は返さない** —— fileId を gog へ渡して読む(guide §4)。
|
|
81
|
+
索引は「SA が読めるメタデータを台帳の受け手へ公開する」もので、本人が Google で読める
|
|
82
|
+
全ファイルではない。complete=false なら**まだ全部ではない**(終了コード 0 でも起こる)。
|
|
75
83
|
`;
|
|
76
84
|
|
|
77
85
|
// ---------------------------------------------------------------------------
|
|
@@ -356,9 +364,17 @@ export function formatFailure(error) {
|
|
|
356
364
|
if (error.status === 404) {
|
|
357
365
|
lines.push(' 対象が無いか、この token の許可範囲外です。403 との違いは guide §3 を見る。');
|
|
358
366
|
}
|
|
367
|
+
if (error.status === 409) {
|
|
368
|
+
// **cursor を組み立て直させない**(索引が作り直された後の続きは存在しない)。
|
|
369
|
+
lines.push(' 索引が更新されました。--cursor を外して取り直してください。');
|
|
370
|
+
}
|
|
359
371
|
if (error.status === 429) {
|
|
360
372
|
lines.push(' 枠(60 回/分・1,000 回/時)を超えました。token は他のワーカーと共有です。');
|
|
361
373
|
}
|
|
374
|
+
if (error.status === 503 && code === 'drive_unavailable') {
|
|
375
|
+
lines.push(' Drive の再確認ができませんでした(一時的)。少し待って同じ要求を出す。');
|
|
376
|
+
lines.push(' 古い一覧を「成功」として返さないので、空ではなく 503 になります。');
|
|
377
|
+
}
|
|
362
378
|
return lines.join('\n');
|
|
363
379
|
}
|
|
364
380
|
|
|
@@ -480,6 +496,65 @@ export function formatLastFailure(channel) {
|
|
|
480
496
|
return `失敗 ${failure.code}${at}`;
|
|
481
497
|
}
|
|
482
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Drive の索引 1 行(`projects resources`)。
|
|
501
|
+
*
|
|
502
|
+
* **文書候補をページ内で先に出す**(§6)。`hints.candidate` が true のものを上へ持ってきて、
|
|
503
|
+
* 名前・MIME・更新日・fileId・入口からの相対位置を示す。並べ替えるのは**このページの中だけ**
|
|
504
|
+
* で、取得順(keyset)そのものは変えない —— 変えると続きのページで取りこぼす。
|
|
505
|
+
*
|
|
506
|
+
* 本文は返らないので、読むときは fileId を gog へ渡す(guide §4)。
|
|
507
|
+
*/
|
|
508
|
+
export function formatDriveResourceLines(response) {
|
|
509
|
+
const lines = [];
|
|
510
|
+
|
|
511
|
+
for (const index of response.indexes ?? []) {
|
|
512
|
+
const parts = [`入口 ${index.resourceId}`];
|
|
513
|
+
parts.push(index.state === 'never' ? '索引なし' : `索引 ${index.state}`);
|
|
514
|
+
if (index.indexedAt) parts.push(`取得 ${formatJst(index.indexedAt)}`);
|
|
515
|
+
// **「全部ある」と読ませない。** complete でなければ理由を必ず添える。
|
|
516
|
+
parts.push(index.complete ? '読み切り' : `未完(${index.reason ?? '理由不明'})`);
|
|
517
|
+
if (index.stale) parts.push('鮮度切れ');
|
|
518
|
+
lines.push(parts.join(' '));
|
|
519
|
+
}
|
|
520
|
+
if ((response.indexes ?? []).length === 0) {
|
|
521
|
+
lines.push('(索引が有効な Drive 入口はありません)');
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const items = [...(response.items ?? [])];
|
|
525
|
+
// 候補を先に(同じ候補の中では取得順のまま)。
|
|
526
|
+
const ordered = [
|
|
527
|
+
...items.filter((item) => item.hints?.candidate === true),
|
|
528
|
+
...items.filter((item) => item.hints?.candidate !== true),
|
|
529
|
+
];
|
|
530
|
+
for (const item of ordered) {
|
|
531
|
+
const mark = item.hints?.candidate ? '*' : ' ';
|
|
532
|
+
const reasons = (item.hints?.reasons ?? []).join(',') || '-';
|
|
533
|
+
const modified = item.modifiedTime ? formatJst(item.modifiedTime) : '-';
|
|
534
|
+
const shortcut = item.shortcut
|
|
535
|
+
? item.shortcut.target
|
|
536
|
+
? ` → ${item.shortcut.target.fileId}`
|
|
537
|
+
: ' → (参照先非公開)'
|
|
538
|
+
: '';
|
|
539
|
+
lines.push(
|
|
540
|
+
`${mark} ${item.fileId} ${item.name} ${item.mimeType} 更新 ${modified} ` +
|
|
541
|
+
`深さ${item.depth} 親=${item.parentFileId} ${reasons}${shortcut}`,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (ordered.length === 0) lines.push('(このページで確認できた資料はありません)');
|
|
545
|
+
|
|
546
|
+
if (response.continuationQuery) {
|
|
547
|
+
// **slug は query に入らない**(path 側)ので、次要求の形にして出す。
|
|
548
|
+
const slug = response.project?.slug ?? '<slug>';
|
|
549
|
+
lines.push(
|
|
550
|
+
`次: stw-agent projects next '${response.continuationQuery}' --slug ${slug}`,
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
// **終端は索引世代の終端**であって、Drive 全件の証明ではない(§6)。
|
|
554
|
+
lines.push('本文は返りません。読むときは fileId を gog へ渡す(guide §4)。');
|
|
555
|
+
return lines.join('\n');
|
|
556
|
+
}
|
|
557
|
+
|
|
483
558
|
export function formatChannelLines(response) {
|
|
484
559
|
const lines = (response.items ?? []).map((channel) => {
|
|
485
560
|
const projects = channel.projectSlugs?.length ? channel.projectSlugs.join(',') : '-';
|
|
@@ -754,13 +829,49 @@ export function statusFromHttp(status) {
|
|
|
754
829
|
async function probeApi(api, requestPath, query) {
|
|
755
830
|
try {
|
|
756
831
|
const payload = await api(requestPath, query);
|
|
757
|
-
|
|
832
|
+
// 応答本体も返す(Drive の検査に案件 slug が要る)。**保持するのは呼び出し側の判断**で、
|
|
833
|
+
// ここでは中身を log にも表示にも出さない。
|
|
834
|
+
return { status: 200, requestId: payload?.requestId ?? null, payload };
|
|
758
835
|
} catch (error) {
|
|
759
836
|
if (error instanceof ApiError) {
|
|
760
|
-
return { status: error.status, requestId: error.payload?.requestId ?? null };
|
|
837
|
+
return { status: error.status, requestId: error.payload?.requestId ?? null, payload: null };
|
|
761
838
|
}
|
|
762
|
-
return { status: null, requestId: null, transport: error?.message ?? 'unknown' };
|
|
839
|
+
return { status: null, requestId: null, payload: null, transport: error?.message ?? 'unknown' };
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* `drive:read` の判定(R1 codex1 #10)。
|
|
845
|
+
*
|
|
846
|
+
* 0.3.0 で `projects resources` を足したのに doctor は `projects:read` と `slack:read` しか
|
|
847
|
+
* 見ておらず、**`drive:read` の無い token でも「権限 OK」**と表示していた。doctor を
|
|
848
|
+
* 通してから新コマンドで 403 になる。
|
|
849
|
+
*
|
|
850
|
+
* Drive の GET は案件 slug を要るので、`projects` の 1 件目を借りて叩く。
|
|
851
|
+
* **flag OFF の 404 と scope 不足の 403 を区別する** —— 404 は「Drive の索引がこの環境で
|
|
852
|
+
* 有効でない(か、この案件に索引が無い)」であって、権限が無いとは言えない。
|
|
853
|
+
*/
|
|
854
|
+
async function probeDriveScope(api, projectsScope, slug) {
|
|
855
|
+
if (projectsScope.status !== 'ok') {
|
|
856
|
+
return { status: 'unknown', label: '不明', why: '(projects:read が未判定)' };
|
|
763
857
|
}
|
|
858
|
+
if (!slug) {
|
|
859
|
+
return { status: 'unknown', label: '不明', why: '(読める案件が無いので未実施)' };
|
|
860
|
+
}
|
|
861
|
+
const drive = await probeApi(api, `/api/agent/projects/${encodeURIComponent(slug)}/resources`, {
|
|
862
|
+
limit: 1,
|
|
863
|
+
});
|
|
864
|
+
if (drive.status === 200) return { status: 'ok', label: 'あり', why: '' };
|
|
865
|
+
if (drive.status === 403) return { status: 'forbidden', label: '権限なし', why: '' };
|
|
866
|
+
if (drive.status === 404) {
|
|
867
|
+
// **権限なしに丸めない。** Drive の索引が無効な環境でも 404 になる。
|
|
868
|
+
return { status: 'unknown', label: '不明', why: '(Drive 索引が無効か、この案件に入口が無い)' };
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
status: 'unknown',
|
|
872
|
+
label: '不明',
|
|
873
|
+
why: drive.status === null ? '(接続できず)' : `(HTTP ${drive.status})`,
|
|
874
|
+
};
|
|
764
875
|
}
|
|
765
876
|
|
|
766
877
|
/**
|
|
@@ -852,10 +963,13 @@ export async function runDoctor({
|
|
|
852
963
|
const slack = await probeApi(api, '/api/agent/slack/channels', { limit: 1 });
|
|
853
964
|
const projectsScope = scopeVerdict(projects.status);
|
|
854
965
|
const slackScope = scopeVerdict(slack.status);
|
|
966
|
+
// Drive は案件 slug が要るので、`projects` の 1 件目を借りる(R1 codex1 #10)。
|
|
967
|
+
const firstSlug = projects.payload?.items?.[0]?.slug ?? null;
|
|
968
|
+
const driveScope = await probeDriveScope(api, projectsScope, firstSlug);
|
|
855
969
|
// **「不明」を「権限なし」に丸めない**(R1 codex2 Medium 3)。403 を見たときだけ
|
|
856
970
|
// 権限なし、全部 200 のときだけ OK、それ以外は不明。API 障害を権限の問題として
|
|
857
971
|
// 報告すると、直し方の案内が嘘になる。
|
|
858
|
-
const verdicts = [projectsScope.status, slackScope.status];
|
|
972
|
+
const verdicts = [projectsScope.status, slackScope.status, driveScope.status];
|
|
859
973
|
const scopeStatus = verdicts.includes('forbidden')
|
|
860
974
|
? 'forbidden'
|
|
861
975
|
: verdicts.every((value) => value === 'ok')
|
|
@@ -868,7 +982,10 @@ export async function runDoctor({
|
|
|
868
982
|
? '(接続できず)'
|
|
869
983
|
: `(HTTP ${slack.status})`
|
|
870
984
|
: '';
|
|
871
|
-
const detail =
|
|
985
|
+
const detail =
|
|
986
|
+
`方式 Bearer service token / projects:read=${projectsScope.label}` +
|
|
987
|
+
` / slack:read=${slackScope.label}${slackWhy}` +
|
|
988
|
+
` / drive:read=${driveScope.label}${driveScope.why}`;
|
|
872
989
|
results.push(
|
|
873
990
|
check(
|
|
874
991
|
'scope',
|
|
@@ -877,9 +994,11 @@ export async function runDoctor({
|
|
|
877
994
|
detail,
|
|
878
995
|
slackScope.status === 'forbidden'
|
|
879
996
|
? 'Slack を読む必要があるなら たま に slack:read と対象チャンネルの許可を頼む'
|
|
880
|
-
:
|
|
881
|
-
? '
|
|
882
|
-
: ''
|
|
997
|
+
: driveScope.status === 'forbidden'
|
|
998
|
+
? 'Drive の索引を読む必要があるなら たま に drive:read と対象入口の許可を頼む'
|
|
999
|
+
: scopeStatus === 'unknown'
|
|
1000
|
+
? '判定できなかった側は API が復旧してから確かめる'
|
|
1001
|
+
: '',
|
|
883
1002
|
),
|
|
884
1003
|
);
|
|
885
1004
|
}
|
|
@@ -1003,7 +1122,16 @@ async function checkBun(execFileImpl, platform, fileExists) {
|
|
|
1003
1122
|
);
|
|
1004
1123
|
}
|
|
1005
1124
|
if (bun.outcome === 'missing') {
|
|
1006
|
-
|
|
1125
|
+
// **入れただけでは PATH に入らない。** installer は `~/.bun/bin` へ置いて
|
|
1126
|
+
// シェルの rc を書き換えるが、非対話シェルや別の rc を使っていると効かない
|
|
1127
|
+
// ——「入れたのに使えない」がまさに Hermes の踏んだ形なので、続きまで書く。
|
|
1128
|
+
return check(
|
|
1129
|
+
'bun',
|
|
1130
|
+
'bun',
|
|
1131
|
+
'missing',
|
|
1132
|
+
'コマンドが無い',
|
|
1133
|
+
`curl -fsSL https://bun.sh/install | bash で入れ、PATH に ${path.dirname(homeBun)} を足す`,
|
|
1134
|
+
);
|
|
1007
1135
|
}
|
|
1008
1136
|
return check('bun', 'bun', 'unknown', `bun --version が exit ${bun.code}`, 'bun の導入を確認する');
|
|
1009
1137
|
}
|
|
@@ -1112,6 +1240,31 @@ const COMMANDS = {
|
|
|
1112
1240
|
},
|
|
1113
1241
|
},
|
|
1114
1242
|
|
|
1243
|
+
/**
|
|
1244
|
+
* Drive 入口配下の索引(2026-09-09 の設計 §6)。
|
|
1245
|
+
*
|
|
1246
|
+
* **`--resource-id` は登録済み入口の UUID** で、Drive の fileId や URL ではない。
|
|
1247
|
+
* 全件取得は `continuationQuery` を `projects next` へ渡して利用者が続ける。
|
|
1248
|
+
*/
|
|
1249
|
+
'projects resources': {
|
|
1250
|
+
options: ['system', 'resource-id', 'limit', 'cursor'],
|
|
1251
|
+
async run({ api, options, positional, json, stdout }) {
|
|
1252
|
+
const slug = positional[0];
|
|
1253
|
+
if (!slug) throw usageError('案件 slug を指定してください');
|
|
1254
|
+
const response = await api(`/api/agent/projects/${encodeURIComponent(slug)}/resources`, {
|
|
1255
|
+
system: options.system,
|
|
1256
|
+
resourceId: options['resource-id'],
|
|
1257
|
+
limit: options.limit,
|
|
1258
|
+
cursor: options.cursor,
|
|
1259
|
+
});
|
|
1260
|
+
stdout.write(
|
|
1261
|
+
json
|
|
1262
|
+
? `${JSON.stringify(response, null, 2)}\n`
|
|
1263
|
+
: `${formatDriveResourceLines(response)}\n`,
|
|
1264
|
+
);
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
|
|
1115
1268
|
'slack channels': {
|
|
1116
1269
|
options: ['project', 'limit', 'cursor'],
|
|
1117
1270
|
async run({ api, options, json, stdout }) {
|
|
@@ -1198,6 +1351,35 @@ const COMMANDS = {
|
|
|
1198
1351
|
);
|
|
1199
1352
|
},
|
|
1200
1353
|
},
|
|
1354
|
+
|
|
1355
|
+
/**
|
|
1356
|
+
* Drive 索引の続き。**cursor を組み立て直さない**(opaque な暗号文なので手で作れない)。
|
|
1357
|
+
*
|
|
1358
|
+
* `continuationQuery` は query だけで**案件 slug を含まない**(slug は path 側にある)。
|
|
1359
|
+
* cursor から復元することもしない —— cursor は認可の証明ではないので、
|
|
1360
|
+
* 「どの案件を読んでいたか」を cursor に語らせない。だから `--slug` を必須にする。
|
|
1361
|
+
*/
|
|
1362
|
+
'projects next': {
|
|
1363
|
+
options: ['slug'],
|
|
1364
|
+
async run({ api, options, positional, json, stdout }) {
|
|
1365
|
+
const query = positional[0];
|
|
1366
|
+
if (!query) {
|
|
1367
|
+
throw usageError('continuationQuery をそのまま渡してください(前回の出力の「次:」行)');
|
|
1368
|
+
}
|
|
1369
|
+
const slug = options.slug;
|
|
1370
|
+
if (!slug) throw usageError('--slug に前回と同じ案件 slug を指定してください');
|
|
1371
|
+
const response = await requestRawQuery(
|
|
1372
|
+
api,
|
|
1373
|
+
`/api/agent/projects/${encodeURIComponent(slug)}/resources`,
|
|
1374
|
+
query,
|
|
1375
|
+
);
|
|
1376
|
+
stdout.write(
|
|
1377
|
+
json
|
|
1378
|
+
? `${JSON.stringify(response, null, 2)}\n`
|
|
1379
|
+
: `${formatDriveResourceLines(response)}\n`,
|
|
1380
|
+
);
|
|
1381
|
+
},
|
|
1382
|
+
},
|
|
1201
1383
|
};
|
|
1202
1384
|
|
|
1203
1385
|
/**
|
package/package.json
CHANGED