shuadouyin 0.3.5 → 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/.dy-build-commit +1 -1
- package/CHANGELOG.md +37 -0
- package/README.md +16 -1
- package/dist/app.js +84 -5
- package/dist/browser.js +5 -1
- package/dist/catalog.js +137 -7
- package/dist/knowledge.js +76 -11
- package/dist/main.js +12 -2
- package/dist/memory/graph.js +38 -22
- package/dist/operations/authorization.js +187 -0
- package/dist/operations/browsing.js +274 -7
- package/dist/operations/chat-history.js +11 -3
- package/dist/operations/checkpoint.js +174 -0
- package/dist/operations/comments.js +5 -0
- package/dist/operations/digest.js +3 -1
- package/dist/operations/facts.js +29 -3
- package/dist/operations/inbox.js +124 -0
- package/dist/operations/live.js +36 -4
- package/dist/operations/observations.js +122 -0
- package/dist/operations/persona-surface.js +126 -0
- package/dist/operations/plan.js +111 -0
- package/dist/operations/playback.js +59 -38
- package/dist/operations/recall.js +97 -0
- package/dist/operations/validate.js +14 -1
- package/dist/page-adapter.js +119 -17
- package/dist/page-coverage.json +1958 -0
- package/dist/workspace.js +18 -0
- package/docs/architecture.md +149 -0
- package/docs/typescript.md +153 -0
- package/package.json +3 -1
- package/skills/dy/SKILL.md +39 -52
- package/skills/dy/references/browse-and-grow.md +60 -2
- package/skills/dy/references/commands.md +27 -3
package/.dy-build-commit
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
7251333bbc6e84fd193536885101c3b6ebf46c86
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.0 — 2026-10-31
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
|
|
7
|
+
- `chat recall` — 撤回自己发的一条私信。平台窗口是 3 分钟;超时后菜单里还有「撤回」但服务端会拒,所以它读响应而不是猜时间。没有二次确认,因此要 `--request-id`。
|
|
8
|
+
- `chat inbox` — 谁在等我回。未读徽标不可靠(打开会话就清),所以按「最后一句是不是对方说的」判断,并把对方分享的作品 id 放进 `shared_works`。
|
|
9
|
+
- `browse checkpoint` — 每批一条:走了哪条路、看到什么、每个交付条件是什么状态、下一步去哪里。进度从这些记录推导,不采信调用方的自述。
|
|
10
|
+
- `browse start --text-file` — 一轮要交付什么、按什么标准判、什么时候停。写一次不能改,改条件就是换一轮。
|
|
11
|
+
- `browse end --outcome` — `completed` 要求 checkpoint 真的达标,否则拒绝;`partial` 是诚实的交付。
|
|
12
|
+
- `persona show` — 按段读取人格,每段带 `complete`。`context show` 从头截断,人格的「身份与声音」在后面,预算不够时会静静切掉。
|
|
13
|
+
- `auth grant` / `auth revoke` / `auth grants` — 记住某个人可以做什么,带期限、每日上限和撤销记录。不是开关。
|
|
14
|
+
|
|
15
|
+
### 改进
|
|
16
|
+
|
|
17
|
+
- `page facts --selector` 只返回问到的标记:4440 → 618 字节。
|
|
18
|
+
- `chat read` 不再复述上一条读到的消息内容:8832 → 4215 字节。
|
|
19
|
+
- `comment list` 去掉每行重复的作品 id 和恒为 false 的布尔:2689 → 1849 字节。
|
|
20
|
+
- `video digest` 不传 `--frames` 时按时长采样。之前两个默认值叠起来,让它从来没生效过。
|
|
21
|
+
- 每个页面命令的返回会带 `persona` 字段,提醒这一轮还没读人格、读的是旧版本、或从没读过。
|
|
22
|
+
- 三个预设各带一组同一事实的对照示例,说明这个声音在四种场景下怎么说话。
|
|
23
|
+
|
|
24
|
+
### 修复
|
|
25
|
+
|
|
26
|
+
- `chat read` 只读视口内的消息。真实群 13 条只返回 7 条,**漏掉的全是别人说的**。
|
|
27
|
+
- `chat inbox` 把「新到旧」的数组当成「旧到新」,`awaiting_reply` 一直是反的。
|
|
28
|
+
- 分享卡的作品 id 读不出来:`parsedContent` 是 getter,而 `own()` 故意拒绝 accessor。
|
|
29
|
+
- `comment list` 和 `comment replies` 的关联信息移到结果顶层,不再每行重复。
|
|
30
|
+
|
|
31
|
+
### 移除
|
|
32
|
+
|
|
33
|
+
- `src/operations/actions.ts` — 提案/批准模型,从未被任何命令或模块引用。它的每日额度已由 `social goal` 和 `auth grant` 覆盖。
|
|
34
|
+
|
|
35
|
+
## 0.3.5 及更早
|
|
36
|
+
|
|
37
|
+
版本号在这一版之前没有随功能推进,所以更早的提交没有对应的版本记录。用 `dy update --check` 的 `commit` 字段判断新旧。
|
package/README.md
CHANGED
|
@@ -62,8 +62,15 @@ npm uninstall -g shuadouyin
|
|
|
62
62
|
|
|
63
63
|
装 skill 给 agent 用(可选):
|
|
64
64
|
|
|
65
|
+
```bash
|
|
66
|
+
npx skills add https://github.com/9Ashwin/ShuaDouyin --skill dy
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
或者用 CLI 自己装到 agent 目录:
|
|
70
|
+
|
|
65
71
|
```bash
|
|
66
72
|
dy skill install
|
|
73
|
+
dy skill status # 已装的副本和仓库是否一致
|
|
67
74
|
```
|
|
68
75
|
|
|
69
76
|
## 开始使用
|
|
@@ -109,7 +116,15 @@ dy video info # 读当前作品
|
|
|
109
116
|
dy guide # 返回完整的使用说明(约 3 万字),agent 读这个
|
|
110
117
|
```
|
|
111
118
|
|
|
112
|
-
或者把
|
|
119
|
+
或者把 skill 装进你的 agent 技能目录,这样它知道什么时候该用 `dy`、怎么用:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx skills add https://github.com/9Ashwin/ShuaDouyin --skill dy
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`skills/dy` 里按主题分成八个参考文件:浏览与成长、记忆与人格、社交、直播、日报、自我改进、命令全表,以及**平台事实**——最后一个是踩过的坑,读它能省掉一次重来。
|
|
126
|
+
|
|
127
|
+
`SKILL.md` 本身只有两百来行,是每次对话都会加载的那部分;参考文件按需读。
|
|
113
128
|
|
|
114
129
|
## 设计原则
|
|
115
130
|
|
package/dist/app.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CliError, normalizeError } from "./errors.js";
|
|
|
5
5
|
import { Knowledge, knowledgeOperation, validateKnowledge } from "./knowledge.js";
|
|
6
6
|
import { migrateLegacyLayout } from "./migrate.js";
|
|
7
7
|
import { askDouyin } from "./operations/ai-search.js";
|
|
8
|
+
import { rounds } from "./operations/browsing.js";
|
|
8
9
|
import { audio, capture, noteImage, noteSlide, observe } from "./operations/capture.js";
|
|
9
10
|
import { captureChatImage, listChats, readChat } from "./operations/chat.js";
|
|
10
11
|
import { compareChatWindows } from "./operations/chat-diff.js";
|
|
@@ -18,11 +19,13 @@ import { inspectCreator } from "./operations/creator.js";
|
|
|
18
19
|
import { digest } from "./operations/digest.js";
|
|
19
20
|
import { pageFactsRead } from "./operations/facts.js";
|
|
20
21
|
import { readImage } from "./operations/image-file.js";
|
|
22
|
+
import { readInbox } from "./operations/inbox.js";
|
|
21
23
|
import { resolveLink } from "./operations/links.js";
|
|
22
24
|
import { liveChatRead, liveGiftSend, liveGifts, liveInfo, liveSay } from "./operations/live.js";
|
|
23
25
|
import { applySearchFilters, closeChatIfOpen, control, listForwardRecipients, listResults, moveFeed, navigate, openRecommendFeed, searchUsers, settleOnWork, sweepFeed, verifySearchFilters, waitSearchResults, waitSnapshot, } from "./operations/navigation.js";
|
|
24
26
|
import { phraseMemories, phraseMemoryText, usePhrase } from "./operations/phrases.js";
|
|
25
27
|
import { readPerson } from "./operations/read-person.js";
|
|
28
|
+
import { recallMessage } from "./operations/recall.js";
|
|
26
29
|
import { userRelations } from "./operations/relations.js";
|
|
27
30
|
import { observeResponse } from "./operations/response-window.js";
|
|
28
31
|
import { skillInstall, skillStatus } from "./operations/skill.js";
|
|
@@ -139,7 +142,7 @@ export class App {
|
|
|
139
142
|
const session = await this.chrome.connect(active, this.timeout, "https://live.douyin.com");
|
|
140
143
|
try {
|
|
141
144
|
const result = operation === "live.say"
|
|
142
|
-
? await liveSay(session, input, active)
|
|
145
|
+
? await liveSay(session, input, this.store, active)
|
|
143
146
|
: await liveGiftSend(session, input, this.store, active);
|
|
144
147
|
await this.store.append(operation, { request_id: input.request_id ?? null, ...result });
|
|
145
148
|
return result;
|
|
@@ -165,6 +168,18 @@ export class App {
|
|
|
165
168
|
timeout: this.timeout,
|
|
166
169
|
});
|
|
167
170
|
}
|
|
171
|
+
// Recalling is its own path for the same reason deleting is: it has no
|
|
172
|
+
// composer, no settlement and no confirmation, so the generic write
|
|
173
|
+
// path would fill an input that does not exist.
|
|
174
|
+
if (operation === "chat.recall") {
|
|
175
|
+
const session = await this.chrome.connect(active, this.timeout);
|
|
176
|
+
try {
|
|
177
|
+
return await recallMessage(session, input, this.store, active);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
await session.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
168
183
|
if (isWrite(operation)) {
|
|
169
184
|
// A failed send used to leave no trace: only the read path recorded
|
|
170
185
|
// failures, so `diagnostics` and the repeated-failure rule in
|
|
@@ -216,6 +231,11 @@ export class App {
|
|
|
216
231
|
// is attached here rather than deep inside each operation so no command
|
|
217
232
|
// has to remember, and so a command added later cannot forget.
|
|
218
233
|
const setup = await setupNotice(this.store.base);
|
|
234
|
+
// The persona note rides along the same way the setup question does.
|
|
235
|
+
// One place attaches it, so a command added later cannot forget, and
|
|
236
|
+
// it is advisory: a caller that has not read the persona yet must
|
|
237
|
+
// still be able to run the command that tells it to.
|
|
238
|
+
const persona = await personaNotice(this.store.base);
|
|
219
239
|
if (operation === "chat.read" && this.store.bot_id) {
|
|
220
240
|
if (!result || typeof result !== "object")
|
|
221
241
|
throw new CliError("state_corrupt", "Chat read did not return a conversation window");
|
|
@@ -227,9 +247,23 @@ export class App {
|
|
|
227
247
|
previous_context: previous,
|
|
228
248
|
};
|
|
229
249
|
}
|
|
230
|
-
if (!["status", "auth.status"].includes(operation))
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
if (!["status", "auth.status"].includes(operation)) {
|
|
251
|
+
// The round is resolved now, while the lock is held and the call is
|
|
252
|
+
// starting. Reading "the open round" after the fact would attach a
|
|
253
|
+
// long sweep to whichever round happened to be open when it ended.
|
|
254
|
+
const open = rounds(await this.store.events()).find((round) => round.status === "open");
|
|
255
|
+
const data = open && result && typeof result === "object"
|
|
256
|
+
? { ...result, _browse: { round_id: open.id, started_at: open.started_at } }
|
|
257
|
+
: result;
|
|
258
|
+
await this.store.append(operation, data);
|
|
259
|
+
}
|
|
260
|
+
const notices = {
|
|
261
|
+
...(setup ? { setup } : {}),
|
|
262
|
+
...(persona ? { persona } : {}),
|
|
263
|
+
};
|
|
264
|
+
return Object.keys(notices).length && result && typeof result === "object"
|
|
265
|
+
? { ...result, ...notices }
|
|
266
|
+
: result;
|
|
233
267
|
}
|
|
234
268
|
catch (error) {
|
|
235
269
|
// Cancellation is the caller's choice and its outcome is not a
|
|
@@ -466,7 +500,7 @@ export class App {
|
|
|
466
500
|
return listForwardRecipients(session, signal);
|
|
467
501
|
}
|
|
468
502
|
case "page.facts":
|
|
469
|
-
return pageFactsRead(session, sourceCheckout().path ?? process.cwd());
|
|
503
|
+
return pageFactsRead(session, sourceCheckout().path ?? process.cwd(), input.target);
|
|
470
504
|
case "live.info":
|
|
471
505
|
return liveInfo(session);
|
|
472
506
|
case "live.chat":
|
|
@@ -604,6 +638,8 @@ export class App {
|
|
|
604
638
|
return { closed: await closeComments(session, signal) };
|
|
605
639
|
case "chat.list":
|
|
606
640
|
return listChats(session);
|
|
641
|
+
case "chat.inbox":
|
|
642
|
+
return readInbox(session, input, signal);
|
|
607
643
|
case "chat.close":
|
|
608
644
|
// Closing is a navigation step: the draft check lives on the page so
|
|
609
645
|
// an unsent message is never discarded by a caller that did not see it.
|
|
@@ -729,4 +765,47 @@ export async function setupNotice(base) {
|
|
|
729
765
|
function publicBrowser(state) {
|
|
730
766
|
return { running: true, headless: state.headless, pid: state.pid, started_at: state.started_at };
|
|
731
767
|
}
|
|
768
|
+
/**
|
|
769
|
+
* Whether the persona was read recently enough to be speaking from it.
|
|
770
|
+
*
|
|
771
|
+
* The skill says to read the persona before the first reply of a session, and a
|
|
772
|
+
* session has no boundary a CLI can see. What it can see is the sequence of
|
|
773
|
+
* commands: a round of work starts when the persona is read, and any page
|
|
774
|
+
* command that happens long after a read — or before the first one ever — is a
|
|
775
|
+
* caller that is probably working without it.
|
|
776
|
+
*
|
|
777
|
+
* This is a note, not a gate. Refusing a read because the persona was not loaded
|
|
778
|
+
* would break the case that matters most: a caller that has not read it yet has
|
|
779
|
+
* to be able to run the command that tells it to.
|
|
780
|
+
*/
|
|
781
|
+
export async function personaNotice(base) {
|
|
782
|
+
const store = new Store(base);
|
|
783
|
+
const events = await store.events();
|
|
784
|
+
const reads = events.filter((event) => event.operation === "persona.read");
|
|
785
|
+
const last = reads.at(-1);
|
|
786
|
+
const current = await new Workspaces(base).document("AGENTS.md").catch(() => null);
|
|
787
|
+
if (!current)
|
|
788
|
+
return null;
|
|
789
|
+
// A persona edited after the last read is stale evidence even if the read was
|
|
790
|
+
// a minute ago: the agent is holding an older version. This is checked before
|
|
791
|
+
// the clock, because "read recently" and "read the current file" are different
|
|
792
|
+
// facts and the second one is the one that matters.
|
|
793
|
+
const lastRevision = last?.data ? last.data.revision : undefined;
|
|
794
|
+
const revisionChanged = !!lastRevision && lastRevision !== current.revision;
|
|
795
|
+
// Half an hour is a guess at "same sitting". A session can be longer, and
|
|
796
|
+
// saying so is better than a rule that silently expires mid-task.
|
|
797
|
+
const stale = !last || Date.now() - Date.parse(last.at) > 30 * 60_000;
|
|
798
|
+
if (!stale && !revisionChanged)
|
|
799
|
+
return null;
|
|
800
|
+
return {
|
|
801
|
+
status: last ? (revisionChanged ? "changed_since_read" : "stale") : "never_read",
|
|
802
|
+
read: "Run `dy persona show` before speaking as this bot. It returns the name, voice and conduct sections with a complete flag per section.",
|
|
803
|
+
revision: current.revision,
|
|
804
|
+
why: last
|
|
805
|
+
? revisionChanged
|
|
806
|
+
? "The persona was edited after it was last read, so anything spoken from the earlier copy is out of date."
|
|
807
|
+
: "No persona read in this stretch of work. The bot's name and voice live in AGENTS.md, and replying without it means replying in the model's own voice."
|
|
808
|
+
: "The persona has never been read on this workspace. Without it the bot speaks as a generic assistant, and nothing in the output says the owner's setting was skipped.",
|
|
809
|
+
};
|
|
810
|
+
}
|
|
732
811
|
//# sourceMappingURL=app.js.map
|
package/dist/browser.js
CHANGED
|
@@ -306,7 +306,11 @@ export class Session {
|
|
|
306
306
|
async call(operation, args = {}) {
|
|
307
307
|
this.signal.throwIfAborted();
|
|
308
308
|
adapter ??= await readFile(new URL("../dist/page-adapter.js", import.meta.url), "utf8");
|
|
309
|
-
|
|
309
|
+
// The injected script is given a name so a profiler can report its coverage.
|
|
310
|
+
// Unnamed scripts arrive with an empty url, and precise coverage is only
|
|
311
|
+
// useful if the ranges can be attributed to this bundle rather than to the
|
|
312
|
+
// page's own code.
|
|
313
|
+
const wire = await this.tab.evaluate(`(() => {\n//# sourceURL=dy-page-adapter.js\n${adapter}\nreturn DouyinPage.call(${JSON.stringify(operation)}, ${JSON.stringify(args)});\n})()`);
|
|
310
314
|
if (wire.error)
|
|
311
315
|
throw new CliError(wire.error.code, wire.error.message, { hint: wire.error.hint });
|
|
312
316
|
return wire.data;
|
package/dist/catalog.js
CHANGED
|
@@ -229,7 +229,7 @@ export const COMMANDS = [
|
|
|
229
229
|
type: "string",
|
|
230
230
|
},
|
|
231
231
|
],
|
|
232
|
-
guidance: "Reports the exact phrases that read as AI, with the reason each one does, plus the markers of a text scrubbed past natural — a comma chain, five equal-length sentences, one judgement sentence after another. Both directions matter: the catalog exists because deleting every tell is its own way of reading as written by a machine. The catalog is adapted from PenguinHarness's Chinese humanizer cues. Advice, not a gate: a flagged phrase kept on purpose is still a choice, and this never rewrites the text.",
|
|
232
|
+
guidance: "Reports the exact phrases that read as AI, with the reason each one does, plus the markers of a text scrubbed past natural — a comma chain, five equal-length sentences, one judgement sentence after another. Both directions matter: the catalog exists because deleting every tell is its own way of reading as written by a machine. The catalog is adapted from PenguinHarness's Chinese humanizer cues. Advice, not a gate: a flagged phrase kept on purpose is still a choice, and this never rewrites the text. It checks general readability only — passing it does not mean the text sounds like this bot, and it says nothing about whether the facts are right or whether the account is a person. Read the persona with persona show for the first question and check the facts yourself for the second.",
|
|
233
233
|
effect: "local",
|
|
234
234
|
},
|
|
235
235
|
{
|
|
@@ -288,6 +288,21 @@ export const COMMANDS = [
|
|
|
288
288
|
guidance: "Replaces the retired line that told the bot to keep the owner's settings fixed and never rewrite the persona. That line contradicts the growth section in the same document, and the older, more concrete line wins, so an agent reading it is told both to grow and never to change. Only the exact retired sentence is matched; a persona whose owner has already rewritten it is left alone. Running it twice changes nothing the second time.",
|
|
289
289
|
effect: "local",
|
|
290
290
|
},
|
|
291
|
+
{
|
|
292
|
+
id: "persona.show",
|
|
293
|
+
description: "Read the persona a caller needs before it speaks, projected by section",
|
|
294
|
+
flags: [
|
|
295
|
+
{
|
|
296
|
+
key: "budget",
|
|
297
|
+
name: "--budget <characters>",
|
|
298
|
+
description: "Room for the persona sections",
|
|
299
|
+
type: "number",
|
|
300
|
+
default: 6000,
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
effect: "local",
|
|
304
|
+
guidance: "Returns the name, identity and voice sections, and the growth and communication rules, each with its own complete flag. Unlike context show, the budget is spent section by section and never cuts a section in half: a section that does not fit comes back empty with complete:false, because half a voice section reads like a whole one. The revision identifies the exact file this was read from. This is the persona as written, not proof an agent followed it — a host that builds its own request can guarantee the text reaches the model, and a skill-only caller cannot.",
|
|
305
|
+
},
|
|
291
306
|
{
|
|
292
307
|
id: "persona.evolve",
|
|
293
308
|
description: "Rewrite the persona's derived voice section from what this account actually wrote",
|
|
@@ -458,17 +473,49 @@ export const COMMANDS = [
|
|
|
458
473
|
default: 5,
|
|
459
474
|
},
|
|
460
475
|
{ key: "goal", name: "--goal <id>", description: "Goal this round serves", type: "string" },
|
|
476
|
+
{
|
|
477
|
+
key: "text_file",
|
|
478
|
+
name: "--text-file <path>",
|
|
479
|
+
description: "Plan JSON: what this round delivers and how it is judged",
|
|
480
|
+
type: "string",
|
|
481
|
+
},
|
|
482
|
+
{ key: "text", name: "--text <json>", description: "The same plan as an inline JSON object", type: "string" },
|
|
483
|
+
],
|
|
484
|
+
effect: "local",
|
|
485
|
+
guidance: "A round left open by a session that died is resumed, not duplicated: two open rounds would each claim a budget and neither would be true. A plan states what this round delivers, which criteria a candidate must meet and what to do when progress stops; it is validated whole before anything is stored and cannot be changed afterwards, so a different plan needs a closed round first. Resuming with a different plan, or with a budget flag that disagrees with the open round, returns revision_conflict rather than merging the two. The budget is advisory — browse round says it is spent, and the caller decides to close.",
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
id: "browse.checkpoint",
|
|
489
|
+
description: "Record what one batch found, what it ruled out, and where it goes next",
|
|
490
|
+
argument: { key: "resource", name: "round-id", description: "Round ID from browse start" },
|
|
491
|
+
flags: [
|
|
492
|
+
{ key: "text_file", name: "--text-file <path>", description: "Checkpoint JSON", type: "string" },
|
|
493
|
+
{ key: "text", name: "--text <json>", description: "The same checkpoint inline", type: "string" },
|
|
494
|
+
{
|
|
495
|
+
key: "if_match",
|
|
496
|
+
name: "--if-match <revision>",
|
|
497
|
+
description: "Revision from browse round",
|
|
498
|
+
type: "string",
|
|
499
|
+
},
|
|
461
500
|
],
|
|
462
501
|
effect: "local",
|
|
463
|
-
guidance: "
|
|
502
|
+
guidance: "One batch is written as a whole or not at all. Each criterion must be supported, not_supported or unknown, and supported or not_supported must cite an event that exists and belongs to an entity in this round; unknown is how a missing answer is recorded. A criterion the plan does not have is refused, because evidence for an unasked question is not evidence. The same checkpoint id with the same content returns the stored result; the same id with different content is idempotency_conflict. Progress is derived from these checkpoints, so a batch that repeats what an earlier one already established does not count as new. This records the caller's assessment; it cannot and does not verify whether the conclusion is true.",
|
|
464
503
|
},
|
|
465
504
|
{
|
|
466
505
|
id: "browse.end",
|
|
467
506
|
description: "Close a browsing round and keep its summary",
|
|
468
507
|
argument: { key: "resource", name: "round-id", description: "Round ID from browse start" },
|
|
469
|
-
flags: [
|
|
508
|
+
flags: [
|
|
509
|
+
{ key: "reason", name: "--reason <text>", description: "Why the round stopped", type: "string" },
|
|
510
|
+
{
|
|
511
|
+
key: "outcome",
|
|
512
|
+
name: "--outcome <kind>",
|
|
513
|
+
description: "completed | partial | blocked | cancelled",
|
|
514
|
+
type: "string",
|
|
515
|
+
},
|
|
516
|
+
],
|
|
470
517
|
effect: "local",
|
|
471
|
-
guidance: "Closing twice is harmless; the first close wins and the second reports already_closed.",
|
|
518
|
+
guidance: "Closing twice is harmless; the first close wins and the second reports already_closed. A round started with a plan must state an outcome, because a round with a delivery condition that ends without one turns partial into done in whatever report reads it later. completed is refused unless the checkpoints actually establish the target count; that is a consistency check on the record, not a judgement about whether the candidates are any good.",
|
|
472
519
|
},
|
|
473
520
|
{
|
|
474
521
|
id: "browse.watch",
|
|
@@ -659,8 +706,16 @@ export const COMMANDS = [
|
|
|
659
706
|
{
|
|
660
707
|
id: "page.facts",
|
|
661
708
|
description: "Compare the page's real data-e2e markers against the ones the code uses",
|
|
709
|
+
flags: [
|
|
710
|
+
{
|
|
711
|
+
key: "target",
|
|
712
|
+
name: "--selector <marker>",
|
|
713
|
+
description: "Only this marker, or a prefix ending in *",
|
|
714
|
+
type: "string",
|
|
715
|
+
},
|
|
716
|
+
],
|
|
662
717
|
effect: "read",
|
|
663
|
-
guidance: "Reports which selectors the adapter relies on that this page does not have, and which markers the page offers that nothing uses. A fixture written by hand cannot catch this: it was written from the same understanding that produced the code. Read-only.",
|
|
718
|
+
guidance: "Reports which selectors the adapter relies on that this page does not have, and which markers the page offers that nothing uses. A fixture written by hand cannot catch this: it was written from the same understanding that produced the code. A page carries about thirty-five markers and a caller usually needs one; --selector <marker> or --selector 'chat*' returns just that, and is what to use when the question is whether a control is present. Works only on the current page; opening a different one first is the caller's job. Read-only.",
|
|
664
719
|
},
|
|
665
720
|
{
|
|
666
721
|
id: "live.info",
|
|
@@ -865,9 +920,12 @@ export const COMMANDS = [
|
|
|
865
920
|
{
|
|
866
921
|
key: "frames",
|
|
867
922
|
name: "--frames <count>",
|
|
868
|
-
|
|
923
|
+
// No default. A default of 3 overrode the duration-aware choice, so
|
|
924
|
+
// every work got three frames whether it was thirty seconds or twenty
|
|
925
|
+
// minutes — which is the thing sampling by duration exists to avoid.
|
|
926
|
+
// Omitted means "choose from the duration"; 0 means none.
|
|
927
|
+
description: "Frames to sample; omit to choose by duration, 0 for none",
|
|
869
928
|
type: "number",
|
|
870
|
-
default: 3,
|
|
871
929
|
},
|
|
872
930
|
{ key: "limit", name: "--limit <count>", description: "Comments to read (default 8)", type: "number" },
|
|
873
931
|
{ key: "output", name: "--output <directory>", description: "Frame output directory", type: "string" },
|
|
@@ -1548,6 +1606,78 @@ export const COMMANDS = [
|
|
|
1548
1606
|
effect: "account_write",
|
|
1549
1607
|
guidance: "Requires --request-id. Stops on identity or draft conflicts. --mention picks one exact name from the group @ picker; an ambiguous or missing name fails instead of mentioning the wrong member. --emoji sends one sticker from the composer's smiley, on its own: combining it with --text is refused because the platform sends a sticker the moment it is clicked. The latest end of a loaded conversation is not yet verifiable, so dispatched messages return delivery_uncertain; inspect the receipt and conversation and do not automatically resend.",
|
|
1550
1608
|
},
|
|
1609
|
+
{
|
|
1610
|
+
id: "auth.grant",
|
|
1611
|
+
description: "Remember that one person may be written to, for a stated span",
|
|
1612
|
+
flags: [
|
|
1613
|
+
{
|
|
1614
|
+
key: "target",
|
|
1615
|
+
name: "--target <id>",
|
|
1616
|
+
description: "user:<sec_uid> or conversation:<id>",
|
|
1617
|
+
type: "string",
|
|
1618
|
+
required: true,
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
key: "actions",
|
|
1622
|
+
name: "--actions <list>",
|
|
1623
|
+
description: "reply, comment, follow, like",
|
|
1624
|
+
type: "string",
|
|
1625
|
+
required: true,
|
|
1626
|
+
},
|
|
1627
|
+
{ key: "days", name: "--days <count>", description: "Days until it lapses; 0 for none", type: "number" },
|
|
1628
|
+
{ key: "per_day", name: "--per-day <count>", description: "Most sends to this target per day", type: "number" },
|
|
1629
|
+
{ key: "reason", name: "--reason <text>", description: "Why this person, for the next reader", type: "string" },
|
|
1630
|
+
],
|
|
1631
|
+
effect: "local",
|
|
1632
|
+
guidance: "This is not a way to switch off asking. It records one target and one set of actions, with an expiry and a reason, because the alternative people actually reach for — a note saying replies are fine — cannot be reviewed or revoked. A grant for one person does not extend to a group they are in, and one for a group does not extend to its members: those are different audiences. Nothing here decides whether a message is worth sending; it only records that the person was named.",
|
|
1633
|
+
},
|
|
1634
|
+
{
|
|
1635
|
+
id: "auth.revoke",
|
|
1636
|
+
description: "Stop a standing permission",
|
|
1637
|
+
flags: [
|
|
1638
|
+
{ key: "resource", name: "--grant-id <id>", description: "ID from auth grants", type: "string", required: true },
|
|
1639
|
+
],
|
|
1640
|
+
effect: "local",
|
|
1641
|
+
guidance: "Revoking does not delete the grant: what it covered already happened, and a later reader needs to know it existed and when it stopped.",
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
id: "auth.grants",
|
|
1645
|
+
description: "List standing permissions and whether each is still in effect",
|
|
1646
|
+
flags: [],
|
|
1647
|
+
effect: "local",
|
|
1648
|
+
guidance: "Shows every grant with its target, actions, expiry and whether it is active. An expired or revoked grant is still listed, because the record of what was permitted is the point.",
|
|
1649
|
+
},
|
|
1650
|
+
{
|
|
1651
|
+
id: "chat.inbox",
|
|
1652
|
+
description: "Read what arrived unread, without deciding what to reply",
|
|
1653
|
+
flags: [
|
|
1654
|
+
{
|
|
1655
|
+
key: "limit",
|
|
1656
|
+
name: "--limit <count>",
|
|
1657
|
+
description: "How many unread conversations to read",
|
|
1658
|
+
type: "number",
|
|
1659
|
+
default: 10,
|
|
1660
|
+
},
|
|
1661
|
+
],
|
|
1662
|
+
effect: "read",
|
|
1663
|
+
guidance: "Lists conversations with unread messages and reads them, so a caller has the content in hand to answer. It does not write a reply and does not generate one. Reading a conversation marks it read on the platform, so a second run may report less than the first — this is an observation, not a subscription. A message sharing a work reports that work's id under shared_works; read it before replying, since a reply about a work nobody watched is the failure this prevents.",
|
|
1664
|
+
},
|
|
1665
|
+
{
|
|
1666
|
+
id: "chat.recall",
|
|
1667
|
+
description: "Retract one of your own messages, inside the platform's time window",
|
|
1668
|
+
flags: [
|
|
1669
|
+
conversation,
|
|
1670
|
+
{
|
|
1671
|
+
key: "message_id",
|
|
1672
|
+
name: "--message-id <id>",
|
|
1673
|
+
description: "Stable ID of a message this account sent",
|
|
1674
|
+
type: "string",
|
|
1675
|
+
required: true,
|
|
1676
|
+
},
|
|
1677
|
+
],
|
|
1678
|
+
effect: "account_write",
|
|
1679
|
+
guidance: "Requires --request-id. Only a message this account sent can be recalled; the page offers no recall entry on anyone else's, so the direction is checked before anything is clicked. The platform allows three minutes: past that the menu entry is still rendered but the request is refused, and this reports invalid_argument rather than guessing from the clock. There is no confirmation dialog, so a successful recall is confirmed by re-reading the conversation and finding the message gone.",
|
|
1680
|
+
},
|
|
1551
1681
|
{
|
|
1552
1682
|
id: "chat.voice",
|
|
1553
1683
|
description: "Ask the page to transcribe voice messages and return the text",
|
package/dist/knowledge.js
CHANGED
|
@@ -8,12 +8,15 @@ import { Growth } from "./growth.js";
|
|
|
8
8
|
import { packMemories } from "./memory/context.js";
|
|
9
9
|
import { Formation } from "./memory/formation.js";
|
|
10
10
|
import { MemoryStore, validateMemoryInput } from "./memory.js";
|
|
11
|
-
import {
|
|
11
|
+
import { grantAccess, grants, revokeAccess } from "./operations/authorization.js";
|
|
12
|
+
import { browsingContext, endRound, noteSkipped, noteWatched, recordCheckpoint, roundRevision, roundSummary, rounds, startRound, } from "./operations/browsing.js";
|
|
12
13
|
import { chatHistory } from "./operations/chat-history.js";
|
|
13
14
|
import { acknowledged, diagnostics, splitByFix } from "./operations/diagnostics.js";
|
|
14
15
|
import { addGoal, discover, pauseGoal } from "./operations/discover.js";
|
|
15
16
|
import { keepFindings } from "./operations/learn.js";
|
|
16
17
|
import { evolvePersona, migratePersonaDocument, personaSection, presetOf } from "./operations/persona-evolve.js";
|
|
18
|
+
import { personaSurface } from "./operations/persona-surface.js";
|
|
19
|
+
import { parsePlan } from "./operations/plan.js";
|
|
17
20
|
import { resetApply, resetPlan } from "./operations/reset.js";
|
|
18
21
|
import { parseSources, signals } from "./operations/signals.js";
|
|
19
22
|
import { closeFollowup, forgetPerson, goalUsage, outcomeSummary, pauseState, recordFollowup, recordOutcome, recordStage, setPaused, socialLog, socialView, } from "./operations/social.js";
|
|
@@ -33,6 +36,11 @@ export function knowledgeOperation(operation) {
|
|
|
33
36
|
// persona.migrate falling through to the browser dispatcher, where the only
|
|
34
37
|
// possible answer was "Unknown operation".
|
|
35
38
|
operation.startsWith("persona.") ||
|
|
39
|
+
// Standing permissions are workspace state: they are recorded in the event
|
|
40
|
+
// log and never touch the page. The whole group, for the same reason the
|
|
41
|
+
// persona group is whole — one leaf listed at a time is how one falls
|
|
42
|
+
// through to the browser dispatcher and answers "Unknown operation".
|
|
43
|
+
operation.startsWith("auth.") ||
|
|
36
44
|
operation.startsWith("learn.") ||
|
|
37
45
|
// social.response reads the page, so it must not be claimed as local: the
|
|
38
46
|
// prefix rule would have made it fall through here and report "unknown
|
|
@@ -157,14 +165,34 @@ export class Knowledge {
|
|
|
157
165
|
return { operation, acknowledged_at: new Date().toISOString() };
|
|
158
166
|
});
|
|
159
167
|
case "browse.start":
|
|
160
|
-
return this.workspaces.lock(() =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
168
|
+
return this.workspaces.lock(async () => {
|
|
169
|
+
// The plan is validated whole before anything is stored, so a bad
|
|
170
|
+
// field cannot leave half a round behind.
|
|
171
|
+
const raw = input.text?.trim() || (input.text_file ? await readFile(input.text_file, "utf8") : "");
|
|
172
|
+
const plan = raw ? parsePlan(JSON.parse(raw)) : undefined;
|
|
173
|
+
const stated = new Set(input._stated ?? []);
|
|
174
|
+
return startRound(new Store(this.workspaces.base), {
|
|
175
|
+
works: input.budget,
|
|
176
|
+
minutes: input.minutes,
|
|
177
|
+
idle: input.idle,
|
|
178
|
+
goal: input.goal,
|
|
179
|
+
...(plan ? { plan } : {}),
|
|
180
|
+
explicit: {
|
|
181
|
+
works: stated.has("budget"),
|
|
182
|
+
minutes: stated.has("minutes"),
|
|
183
|
+
idle: stated.has("idle"),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
case "browse.checkpoint":
|
|
188
|
+
return this.workspaces.lock(async () => {
|
|
189
|
+
const raw = input.text?.trim() || (input.text_file ? await readFile(input.text_file, "utf8") : "");
|
|
190
|
+
if (!raw)
|
|
191
|
+
throw new CliError("invalid_argument", "Supply the batch with --text-file or --text");
|
|
192
|
+
return recordCheckpoint(new Store(this.workspaces.base), input.resource ?? "", JSON.parse(raw), input.if_match);
|
|
193
|
+
});
|
|
166
194
|
case "browse.end":
|
|
167
|
-
return this.workspaces.lock(() => endRound(new Store(this.workspaces.base), input.resource ?? "", input.reason ?? ""));
|
|
195
|
+
return this.workspaces.lock(() => endRound(new Store(this.workspaces.base), input.resource ?? "", input.reason ?? "", input.outcome));
|
|
168
196
|
case "browse.watch":
|
|
169
197
|
return this.workspaces.lock(() => noteWatched(new Store(this.workspaces.base), input.round ?? "", input.video_id ?? ""));
|
|
170
198
|
case "browse.skip":
|
|
@@ -173,9 +201,15 @@ export class Knowledge {
|
|
|
173
201
|
return this.workspaces.lock(async () => {
|
|
174
202
|
const store = new Store(this.workspaces.base);
|
|
175
203
|
const events = await store.events();
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
204
|
+
if (!input.resource)
|
|
205
|
+
return { rounds: rounds(events).slice(-(input.limit ?? 10)) };
|
|
206
|
+
const summary = roundSummary(events, input.resource);
|
|
207
|
+
if (!summary)
|
|
208
|
+
return { round_id: input.resource, known: false };
|
|
209
|
+
// The revision is what a checkpoint's --if-match names. It covers the
|
|
210
|
+
// plan and every checkpoint written so far, so a batch prepared
|
|
211
|
+
// against this view is refused if another one landed in between.
|
|
212
|
+
return { ...summary, revision: roundRevision(events, input.resource) };
|
|
179
213
|
});
|
|
180
214
|
case "context.browsing":
|
|
181
215
|
return this.workspaces.lock(async () => browsingContext(await new Store(this.workspaces.base).events(), { limit: input.limit }));
|
|
@@ -289,6 +323,23 @@ export class Knowledge {
|
|
|
289
323
|
}
|
|
290
324
|
case "learn.findings":
|
|
291
325
|
return this.workspaces.document("lessons.md");
|
|
326
|
+
case "persona.show": {
|
|
327
|
+
// Read-only and uncached: the revision returned has to be the one the
|
|
328
|
+
// text came from, or a caller has no way to tell an edit happened
|
|
329
|
+
// between reading the persona and using it.
|
|
330
|
+
const document = await this.workspaces.document("AGENTS.md");
|
|
331
|
+
const surface = personaSurface(BOT_ID, "AGENTS.md", document.revision, document.content, input.budget ?? 6000);
|
|
332
|
+
// The read is recorded. A command that runs afterwards can then tell
|
|
333
|
+
// whether the persona was picked up in this session, which is the
|
|
334
|
+
// difference between "the file is there" and "the agent has it". The
|
|
335
|
+
// record is the revision, so an edit between reading and using is
|
|
336
|
+
// visible rather than assumed away.
|
|
337
|
+
await new Store(this.workspaces.base).append("persona.read", {
|
|
338
|
+
revision: document.revision,
|
|
339
|
+
complete: surface.complete,
|
|
340
|
+
});
|
|
341
|
+
return surface;
|
|
342
|
+
}
|
|
292
343
|
case "persona.snapshot": {
|
|
293
344
|
const document = await this.workspaces.document("AGENTS.md");
|
|
294
345
|
const version = await this.workspaces.snapshot("AGENTS.md", document.content);
|
|
@@ -320,6 +371,20 @@ export class Knowledge {
|
|
|
320
371
|
return new Growth(this.workspaces).acknowledge(input.event_id ?? "");
|
|
321
372
|
case "growth.event":
|
|
322
373
|
return this.workspaces.lock(() => new Growth(this.workspaces).event(input.event_id ?? ""));
|
|
374
|
+
case "auth.grant":
|
|
375
|
+
return this.workspaces.lock(() => grantAccess(new Store(this.workspaces.base), {
|
|
376
|
+
target: input.target,
|
|
377
|
+
actions: input.actions,
|
|
378
|
+
days: input.days,
|
|
379
|
+
per_day: input.per_day,
|
|
380
|
+
reason: input.reason,
|
|
381
|
+
}));
|
|
382
|
+
case "auth.revoke":
|
|
383
|
+
return this.workspaces.lock(() => revokeAccess(new Store(this.workspaces.base), input.resource ?? input.target ?? ""));
|
|
384
|
+
case "auth.grants":
|
|
385
|
+
return this.workspaces.lock(async () => ({
|
|
386
|
+
grants: grants(await new Store(this.workspaces.base).events()),
|
|
387
|
+
}));
|
|
323
388
|
case "bot.show":
|
|
324
389
|
return this.workspaces.lock(async () => ({
|
|
325
390
|
id: BOT_ID,
|
package/dist/main.js
CHANGED
|
@@ -94,12 +94,22 @@ export function createProgram(io, parentSignal, version = "") {
|
|
|
94
94
|
const { app: appOptions, globals } = options(command);
|
|
95
95
|
const input = { dry_run: globals.dryRun, request_id: globals.requestId };
|
|
96
96
|
const local = command.opts();
|
|
97
|
+
// Which flags the caller actually typed, as opposed to the defaults the
|
|
98
|
+
// catalog fills in. Resuming a round has to tell "the caller wants thirty
|
|
99
|
+
// works" from "commander put thirty here because nothing was said", or a
|
|
100
|
+
// resumed five-minute round quietly becomes a thirty-minute one.
|
|
101
|
+
const stated = [];
|
|
97
102
|
for (const flag of spec.flags ?? []) {
|
|
98
103
|
const option = new Option(flag.name);
|
|
99
104
|
const value = local[option.attributeName()];
|
|
100
|
-
if (value
|
|
101
|
-
|
|
105
|
+
if (value === undefined)
|
|
106
|
+
continue;
|
|
107
|
+
Object.assign(input, { [flag.key]: value });
|
|
108
|
+
if (command.getOptionValueSource(option.attributeName()) === "cli")
|
|
109
|
+
stated.push(flag.key);
|
|
102
110
|
}
|
|
111
|
+
if (stated.length)
|
|
112
|
+
Object.assign(input, { _stated: stated });
|
|
103
113
|
if (spec.argument)
|
|
104
114
|
Object.assign(input, { [spec.argument.key]: values[0] });
|
|
105
115
|
const signal = AbortSignal.any([
|