@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 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` / `slack channels` /
39
- `slack messages` / `slack next` の 6 つ。正確なオプションは常に `--help` を見る
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
- return { status: 200, requestId: payload?.requestId ?? null };
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 = `方式 Bearer service token / projects:read=${projectsScope.label} / slack:read=${slackScope.label}${slackWhy}`;
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
- : scopeStatus === 'unknown'
881
- ? '判定できなかった側は API が復旧してから確かめる'
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
- return check('bun', 'bun', 'missing', 'コマンドが無い', 'curl -fsSL https://bun.sh/install | bash');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shimatoworks/stw-agent",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "CLI for the Shimatoworks project-context read API (project registry entries and Slack discussion).",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",