@primusclaw/personal-agent 0.6.0 → 0.7.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/dist/index.js CHANGED
@@ -4990,6 +4990,7 @@ var ITEM_KINDS = ["task", "review", "remote"];
4990
4990
 
4991
4991
  // ../claw/packages/protocol/src/claw-api/assistant-events.ts
4992
4992
  var ASSISTANT_EVENT_TYPES = ["assistant.say", "work_item.needs_you", "assistant.action"];
4993
+ var TERMINAL_ASKS = ["after_card", "on_input"];
4993
4994
  var REVIEW_HANDOFF_ACTION = "review_for_other";
4994
4995
 
4995
4996
  // ../claw/packages/protocol/src/claw-api/notifications.ts
@@ -6137,8 +6138,8 @@ function writtenLang(text8) {
6137
6138
  const words3 = written3.map((w) => w.toLowerCase());
6138
6139
  if (words3.length < 3) return void 0;
6139
6140
  const first = words3[0] ?? "";
6140
- const sentence2 = EN_VERBS.has(first) || words3.some((w) => EN_GLUE.has(w)) || written3.some((w) => EN_FORM.test(w));
6141
- return sentence2 ? "en" : void 0;
6141
+ const sentence3 = EN_VERBS.has(first) || words3.some((w) => EN_GLUE.has(w)) || written3.some((w) => EN_FORM.test(w));
6142
+ return sentence3 ? "en" : void 0;
6142
6143
  }
6143
6144
  function pickLang2(...texts4) {
6144
6145
  for (const text8 of texts4) {
@@ -9557,21 +9558,34 @@ function helpLang(store, userId, locale, asked3) {
9557
9558
  const written3 = asked3 !== void 0 && !prefs.langSet ? writtenLang(asked3) : void 0;
9558
9559
  return written3 ?? langOf2(store, userId, locale);
9559
9560
  }
9561
+ function groupLangOf(store, conversationId, userId, locale, written3) {
9562
+ const chosen = groupChosenLang(store, conversationId);
9563
+ if (chosen !== void 0) return chosen;
9564
+ const prefs = userId ? store.prefs.getPrefs(userId) : void 0;
9565
+ return writtenLang(written3) ?? answerLang(prefs !== void 0 && !prefs.langSet ? prefs.lang : "", locale);
9566
+ }
9567
+ function groupChosenLang(store, conversationId) {
9568
+ const lang = conversationId ? store.groupLanguage?.groupLanguage(conversationId) : null;
9569
+ return lang === "zh" || lang === "en" ? lang : void 0;
9570
+ }
9560
9571
  var Chat = class {
9561
- constructor(store, sender, conversation, scope, locale) {
9572
+ constructor(store, sender, conversation, scope, locale, written3 = "") {
9562
9573
  this.store = store;
9563
9574
  this.sender = sender;
9564
9575
  this.conversation = conversation;
9565
9576
  this.scope = scope;
9566
9577
  this.locale = locale;
9578
+ this.written = written3;
9567
9579
  }
9568
9580
  store;
9569
9581
  sender;
9570
9582
  conversation;
9571
9583
  scope;
9572
9584
  locale;
9585
+ written;
9586
+ /** In a 1:1 chat the person's language (`langOf`); in a group, the group's (`groupLangOf`). */
9573
9587
  lang() {
9574
- return langOf2(this.store, this.sender.id, this.locale);
9588
+ return this.scope ? groupLangOf(this.store, this.scope, this.sender.id, this.locale, this.written) : langOf2(this.store, this.sender.id, this.locale);
9575
9589
  }
9576
9590
  zh() {
9577
9591
  return this.lang() === "zh";
@@ -11017,7 +11031,12 @@ function renderHelp(language, options = {}) {
11017
11031
  const oneToOne = chat.group ? oneToOneBlock(lang, chat, bot) : void 0;
11018
11032
  if (oneToOne !== void 0) out.push(oneToOne);
11019
11033
  out.push(authorityText(zh, chat.parts));
11020
- if (chat.group) out.push(zh ? "\u8BBE\u7F6E\u53EA\u80FD\u5728\u548C\u6211\u7684 1:1 \u804A\u5929\u91CC\u6539\u3002" : "Settings are changed in a 1:1 chat with me.");
11034
+ if (chat.group)
11035
+ out.push(
11036
+ zh ? `\u8BBE\u7F6E\u53EA\u80FD\u5728\u548C\u6211\u7684 1:1 \u804A\u5929\u91CC\u6539\u3002
11037
+ \u53EA\u6709\u8FD9\u4E2A\u7FA4\u7684\u56DE\u590D\u8BED\u8A00\u5728\u7FA4\u91CC\u8BBE\uFF1A@${bot} \u8BF4\u300C\u8FD9\u4E2A\u7FA4\u7528\u82F1\u6587\u300D\uFF08\u6216\u4E2D\u6587\u3001\u8DDF\u968F\u63D0\u95EE\u8005\uFF09\u3002` : `Settings are changed in a 1:1 chat with me.
11038
+ This group's language is set here: @${bot} "use English in this group" (or Chinese, or "follow the asker").`
11039
+ );
11021
11040
  out.push(dataStatement(lang, !chat.group && chat.parts.has("remote"), options.groupContext));
11022
11041
  out.push(
11023
11042
  zh ? "\u968F\u65F6\u95EE\u6211\u300C\u4F60\u80FD\u505A\u4EC0\u4E48\u300D\u770B\u8FD9\u4EFD\u8BF4\u660E\u3002\u5E95\u90E8\u83DC\u5355\u91CC\u4E5F\u6709\u5E38\u7528\u7684\u6309\u94AE\u3002" : 'Ask "what can you do" any time to see this again. The menu by the compose box has the common ones too.'
@@ -11600,8 +11619,8 @@ var VALUED = [
11600
11619
  function personal(text8, flat2, key) {
11601
11620
  if (STOP_IT2.has(key)) return "/stop";
11602
11621
  if (DETACH2.has(detachKey(key))) return "/agent detach";
11603
- const phrase = lookUp(key, PERSONAL_PHRASES);
11604
- if (phrase !== null) return phrase;
11622
+ const phrase2 = lookUp(key, PERSONAL_PHRASES);
11623
+ if (phrase2 !== null) return phrase2;
11605
11624
  for (const read of VALUED) {
11606
11625
  const command = read(flat2);
11607
11626
  if (command !== null) return command;
@@ -12095,11 +12114,13 @@ var RouterContext = class {
12095
12114
  /** The chat an event is answered in: its conversation, as the person who sent it. */
12096
12115
  chatOf(event, scope) {
12097
12116
  const conversation = this.deps.surface.conversation(event.conversation);
12098
- return new Chat(this.deps.store, event.sender, conversation, scope, event.locale);
12117
+ const written3 = event.type === "message" ? event.text : "";
12118
+ return new Chat(this.deps.store, event.sender, conversation, scope, event.locale, written3);
12099
12119
  }
12100
12120
  /** A chat in another conversation of the same person (their 1:1 chat, a group a request waited in). */
12101
12121
  chatIn(chat, ref, scope = "") {
12102
- return new Chat(this.deps.store, chat.sender, this.deps.surface.conversation(ref), scope, chat.locale);
12122
+ const written3 = scope ? chat.written : "";
12123
+ return new Chat(this.deps.store, chat.sender, this.deps.surface.conversation(ref), scope, chat.locale, written3);
12103
12124
  }
12104
12125
  /** A click as a feature handler is given it. */
12105
12126
  clickRoute(action, chat) {
@@ -12152,8 +12173,8 @@ var groupText = defineCatalog({
12152
12173
  stopMany: (p) => `#${String(p.number)}: more than one repository has it; write it as owner/repo#${String(p.number)}.`
12153
12174
  },
12154
12175
  zh: {
12155
- githubOneToOne: "`/github` works only in a 1:1 chat. Please do this in a 1:1 chat with me.",
12156
- oneToOneOnly: (p) => `\`${p.word}\` works only in a 1:1 chat. Please do this in a 1:1 chat with me.`,
12176
+ githubOneToOne: "`/github` \u53EA\u80FD\u5728 1:1 \u804A\u5929\u91CC\u7528\uFF0C\u8BF7\u5728\u548C\u6211\u7684 1:1 \u804A\u5929\u91CC\u64CD\u4F5C\u3002",
12177
+ oneToOneOnly: (p) => `\`${p.word}\` \u53EA\u80FD\u5728 1:1 \u804A\u5929\u91CC\u7528\uFF0C\u8BF7\u5728\u548C\u6211\u7684 1:1 \u804A\u5929\u91CC\u64CD\u4F5C\u3002`,
12157
12178
  agentInGroup: "`/agent` \u5728\u7FA4\u91CC\u53EA\u80FD\u7528 attach \u548C detach\uFF0C\u5176\u4F59\u8BF7\u5728 1:1 \u804A\u5929\u91CC\u7528\u3002",
12158
12179
  whoami: (p) => `${p.name || "\u4F60"} \u5DF2\u8FDE\u63A5\u5230 SaFE \u7528\u6237 ${p.who}\u3002`,
12159
12180
  whoamiUnknown: "\u4F60\u5DF2\u8FDE\u63A5\uFF0C\u4F46 SaFE \u6CA1\u544A\u8BC9\u6211\u662F\u8C01\u7684\u8D26\u53F7\u3002",
@@ -12319,7 +12340,8 @@ var Onboarding = class {
12319
12340
  try {
12320
12341
  const conversation = await this.openPersonal(chat);
12321
12342
  if (conversation === void 0) throw new Error("no 1:1 conversation");
12322
- await this.ctx.chatIn(chat, conversation.ref).say(groupText.in(chat.lang()).targeted({ bot: config.botName }));
12343
+ const dm = this.ctx.chatIn(chat, conversation.ref);
12344
+ await dm.say(groupText.in(dm.lang()).targeted({ bot: config.botName }));
12323
12345
  } catch (error) {
12324
12346
  logger.warn("group: the 1:1 line about a private message did not go out", { error: errorName(error) });
12325
12347
  return;
@@ -13070,6 +13092,208 @@ var SettingsCommands = class {
13070
13092
  }
13071
13093
  };
13072
13094
 
13095
+ // packages/core/src/language/group.ts
13096
+ var WORDS = /* @__PURE__ */ new Map([
13097
+ ["zh", "zh"],
13098
+ ["cn", "zh"],
13099
+ ["chinese", "zh"],
13100
+ ["\u4E2D\u6587", "zh"],
13101
+ ["\u6C49\u8BED", "zh"],
13102
+ ["en", "en"],
13103
+ ["english", "en"],
13104
+ ["\u82F1\u6587", "en"],
13105
+ ["\u82F1\u8BED", "en"],
13106
+ ["auto", "auto"],
13107
+ ["follow", "auto"],
13108
+ ["\u81EA\u52A8", "auto"],
13109
+ ["\u8DDF\u968F", "auto"],
13110
+ ["\u8DDF\u968F\u63D0\u95EE\u8005", "auto"],
13111
+ ["followtheasker", "auto"]
13112
+ ]);
13113
+ var PUNCT2 = ` \r
13114
+ \u3002\uFF0E.\uFF01!\uFF1F?\uFF5E~\uFF0C,\u3001\uFF1B;\uFF1A:"'\u201C\u201D\u2018\u2019\uFF08\uFF09()`;
13115
+ var GROUP = "(?:\u8FD9\u4E2A\u7FA4\u804A|\u8FD9\u4E2A\u7FA4|\u672C\u7FA4|\u6B64\u7FA4|\u7FA4\u804A|\u7FA4)(?:\u91CC\u9762|\u91CC|\u5185|\u4E2D)?";
13116
+ var LANG_ZH = "(\u4E2D\u6587|\u6C49\u8BED|\u82F1\u6587|\u82F1\u8BED)";
13117
+ var ASKER = "(?:\u63D0\u95EE\u8005|\u63D0\u95EE\u4EBA|\u63D0\u95EE\u7684\u4EBA|\u95EE\u95EE\u9898\u7684\u4EBA|\u95EE\u7684\u4EBA|\u53D1\u95EE\u7684\u4EBA)";
13118
+ var ZH_SET = [
13119
+ new RegExp(
13120
+ `^(?:\u4EE5\u540E|\u4E4B\u540E|\u4ECA\u540E|\u4ECE\u73B0\u5728\u8D77|\u4ECE\u73B0\u5728\u5F00\u59CB)?(?:\u8BF7|\u9EBB\u70E6)?(?:\u5728)?${GROUP}(?:\u4EE5\u540E|\u4E4B\u540E|\u4ECA\u540E|\u4ECE\u73B0\u5728\u8D77|\u4ECE\u73B0\u5728\u5F00\u59CB)?(?:\u8BF7)?(?:\u90FD|\u4E00\u5F8B|\u7EDF\u4E00)?(?:\u7528|\u8BF4|\u4F7F\u7528)${LANG_ZH}(?:\u56DE\u590D|\u56DE\u7B54|\u4EA4\u6D41|\u8BF4\u8BDD|\u804A\u5929|\u804A)?(?:\u5427|\u597D\u5417|\u53EF\u4EE5\u5417)?$`,
13121
+ "u"
13122
+ ),
13123
+ new RegExp(
13124
+ `^(?:\u628A)?${GROUP}(?:\u7684)?(?:\u56DE\u590D)?\u8BED\u8A00(?:\u8BBE\u4E3A|\u8BBE\u6210|\u6539\u6210|\u6539\u4E3A|\u8BBE\u7F6E\u4E3A|\u8BBE\u7F6E\u6210|\u5207\u6362\u5230|\u5207\u6362\u6210|\u6362\u6210)${LANG_ZH}$`,
13125
+ "u"
13126
+ )
13127
+ ];
13128
+ var ZH_AUTO = [
13129
+ new RegExp(
13130
+ `^(?:\u4EE5\u540E|\u4E4B\u540E|\u4ECA\u540E)?(?:\u5728)?${GROUP}(?:\u4EE5\u540E|\u4E4B\u540E|\u4ECA\u540E)?(?:\u90FD)?(?:\u8DDF\u968F|\u8DDF\u7740|\u6309|\u6309\u7167|\u7528)${ASKER}(?:\u7684)?(?:\u8BED\u8A00)?(?:\u56DE\u590D|\u56DE\u7B54)?(?:\u5427)?$`,
13131
+ "u"
13132
+ ),
13133
+ new RegExp(
13134
+ `^(?:\u628A)?${GROUP}(?:\u7684)?(?:\u56DE\u590D)?\u8BED\u8A00(?:\u8BBE\u4E3A|\u8BBE\u6210|\u6539\u6210|\u6539\u4E3A|\u8BBE\u7F6E\u4E3A|\u8BBE\u7F6E\u6210|\u5207\u6362\u5230|\u5207\u6362\u6210|\u6362\u6210)(?:\u81EA\u52A8|\u8DDF\u968F${ASKER})$`,
13135
+ "u"
13136
+ )
13137
+ ];
13138
+ var LANG_EN = "(english|chinese)";
13139
+ var THIS_GROUP = "(?:this|the|our) (?:group chat|group|chat)";
13140
+ var ANSWER = "(?:use|speak|reply in|answer in|respond in|talk in|write in)";
13141
+ var EN_SET = [
13142
+ new RegExp(`^(?:please )?${ANSWER} ${LANG_EN} (?:in|for) ${THIS_GROUP}(?: from now on)?(?: please)?$`),
13143
+ new RegExp(`^(?:in|for) ${THIS_GROUP},? (?:please )?${ANSWER} ${LANG_EN}(?: from now on)?(?: please)?$`),
13144
+ new RegExp(`^${THIS_GROUP} (?:uses|speaks|is in) ${LANG_EN}$`),
13145
+ new RegExp(
13146
+ `^(?:please )?(?:set|change|switch) (?:the |this )?(?:group|chat)(?:'s)? language to ${LANG_EN}(?: please)?$`
13147
+ )
13148
+ ];
13149
+ var EN_AUTO = [
13150
+ new RegExp(
13151
+ `^(?:please )?(?:follow the asker|use the asker'?s language|reply in the asker'?s language|answer in the language of each question)(?: (?:in|for) ${THIS_GROUP})?(?: please)?$`
13152
+ ),
13153
+ new RegExp(
13154
+ `^(?:please )?(?:set|change|switch) (?:the |this )?(?:group|chat)(?:'s)? language to (?:auto|follow the asker)(?: please)?$`
13155
+ )
13156
+ ];
13157
+ function phrase(text8) {
13158
+ return pySplit(pyLower(pyStrip(text8, PUNCT2))).join("");
13159
+ }
13160
+ function sentence(text8) {
13161
+ return pySplit(pyLower(pyStrip(text8, PUNCT2))).join(" ");
13162
+ }
13163
+ var MAX_SENTENCE_CHARS = 120;
13164
+ function sentenceCommand(text8) {
13165
+ if (text8.length > MAX_SENTENCE_CHARS) return void 0;
13166
+ const zh = phrase(text8);
13167
+ const en2 = sentence(text8);
13168
+ for (const pattern of ZH_SET) {
13169
+ const word2 = pattern.exec(zh)?.[1];
13170
+ const lang = word2 === void 0 ? void 0 : WORDS.get(word2);
13171
+ if (lang !== void 0) return { kind: "set", lang };
13172
+ }
13173
+ if (ZH_AUTO.some((p) => p.test(zh))) return { kind: "set", lang: "auto" };
13174
+ for (const pattern of EN_SET) {
13175
+ const word2 = pattern.exec(en2)?.[1];
13176
+ const lang = word2 === void 0 ? void 0 : WORDS.get(word2);
13177
+ if (lang !== void 0) return { kind: "set", lang };
13178
+ }
13179
+ if (EN_AUTO.some((p) => p.test(en2))) return { kind: "set", lang: "auto" };
13180
+ return void 0;
13181
+ }
13182
+ function parseGroupLang(text8) {
13183
+ const words3 = pySplit(text8);
13184
+ if (pyLower(words3[0] ?? "") !== "/lang") return sentenceCommand(pyStrip(text8));
13185
+ if (words3.length === 1) return { kind: "status" };
13186
+ const lang = WORDS.get(pyLower(words3.slice(1).join("")));
13187
+ return lang === void 0 ? { kind: "usage" } : { kind: "set", lang };
13188
+ }
13189
+
13190
+ // packages/core/src/locales/group-language.ts
13191
+ var NAMES2 = {
13192
+ zh: ["\u4E2D\u6587", "Chinese"],
13193
+ en: ["\u82F1\u6587", "English"],
13194
+ auto: ["\u8DDF\u968F\u63D0\u95EE\u8005\u7684\u8BED\u8A00", "the language each question is asked in"]
13195
+ };
13196
+ function groupLanguageSet(lang, name) {
13197
+ const [zh, en2] = NAMES2[lang];
13198
+ return `\u5DF2\u6539\u4E3A${zh}\u56DE\u590D\uFF0C\u7531 ${name || "\u7FA4\u6210\u5458"} \u8BBE\u7F6E\u3002
13199
+ I answer in ${en2} in this group from now on (set by ${name || "a member"}).`;
13200
+ }
13201
+ var GROUP_LANGUAGE_CARD = {
13202
+ title: "\u8FD9\u4E2A\u7FA4\u7528\u54EA\u79CD\u8BED\u8A00\u56DE\u590D\uFF1F/ Which language should I use in this group?",
13203
+ text: (p) => `\u7FA4\u91CC\u4EFB\u4F55\u4EBA\u90FD\u53EF\u4EE5\u9009\uFF0C\u4E4B\u540E\u4E5F\u80FD\u7528 @${p.bot} /lang zh | en | auto \u518D\u6539\u3002
13204
+ Anyone here may pick; @${p.bot} /lang zh | en | auto changes it later.`,
13205
+ buttons: { zh: "\u4E2D\u6587", en: "English", auto: "\u8DDF\u968F\u63D0\u95EE\u8005 (Follow the asker)" }
13206
+ };
13207
+ var groupLanguageText = defineCatalog({
13208
+ en: {
13209
+ isZh: (p) => `This group is answered in Chinese. @${p.bot} /lang en or /lang auto changes it.`,
13210
+ isEn: (p) => `This group is answered in English. @${p.bot} /lang zh or /lang auto changes it.`,
13211
+ isAuto: (p) => `This group is answered in the language each question is asked in. @${p.bot} /lang zh or /lang en sets one.`,
13212
+ isNone: (p) => `This group has no language set: each question is answered in the language it is asked in. @${p.bot} /lang zh, /lang en or /lang auto sets it.`,
13213
+ usage: (p) => `Usage: @${p.bot} /lang zh | en | auto (follow the asker).`
13214
+ },
13215
+ zh: {
13216
+ isZh: (p) => `\u8FD9\u4E2A\u7FA4\u7528\u4E2D\u6587\u56DE\u590D\u3002@${p.bot} /lang en \u6216 /lang auto \u53EF\u6539\u3002`,
13217
+ isEn: (p) => `\u8FD9\u4E2A\u7FA4\u7528\u82F1\u6587\u56DE\u590D\u3002@${p.bot} /lang zh \u6216 /lang auto \u53EF\u6539\u3002`,
13218
+ isAuto: (p) => `\u8FD9\u4E2A\u7FA4\u6309\u6BCF\u4E2A\u95EE\u9898\u6240\u7528\u7684\u8BED\u8A00\u56DE\u590D\u3002@${p.bot} /lang zh \u6216 /lang en \u53EF\u8BBE\u4E3A\u56FA\u5B9A\u8BED\u8A00\u3002`,
13219
+ isNone: (p) => `\u8FD9\u4E2A\u7FA4\u8FD8\u6CA1\u8BBE\u8BED\u8A00\uFF1A\u6BCF\u4E2A\u95EE\u9898\u6309\u5B83\u6240\u7528\u7684\u8BED\u8A00\u56DE\u590D\u3002@${p.bot} /lang zh\u3001/lang en \u6216 /lang auto \u53EF\u8BBE\u7F6E\u3002`,
13220
+ usage: (p) => `\u7528\u6CD5\uFF1A@${p.bot} /lang zh | en | auto\uFF08\u8DDF\u968F\u63D0\u95EE\u8005\uFF09\u3002`
13221
+ }
13222
+ });
13223
+
13224
+ // packages/core/src/routing/group-language.ts
13225
+ var GROUP_LANG_VERB = "group_lang";
13226
+ var LANGS2 = ["zh", "en", "auto"];
13227
+ function isGroupLanguage(value) {
13228
+ return typeof value === "string" && LANGS2.includes(value);
13229
+ }
13230
+ var GroupLanguages = class {
13231
+ constructor(ctx) {
13232
+ this.ctx = ctx;
13233
+ }
13234
+ ctx;
13235
+ /** `/lang …` or a sentence that sets the group's language: answered here; false when `text` is neither. */
13236
+ async command(chat, text8) {
13237
+ const command = parseGroupLang(text8);
13238
+ if (command === void 0) return false;
13239
+ const bot = this.ctx.deps.config.botName;
13240
+ if (command.kind === "set") {
13241
+ this.ctx.deps.store.groupLanguage.setGroupLanguage(chat.scope, command.lang);
13242
+ await chat.say(groupLanguageSet(command.lang, chat.sender.name));
13243
+ return true;
13244
+ }
13245
+ const said2 = groupLanguageText.in(chat.lang());
13246
+ if (command.kind === "usage") {
13247
+ await chat.say(said2.usage({ bot }));
13248
+ return true;
13249
+ }
13250
+ const lang = this.ctx.deps.store.groupLanguage.groupLanguage(chat.scope);
13251
+ const state = lang === "zh" ? said2.isZh : lang === "en" ? said2.isEn : lang === "auto" ? said2.isAuto : said2.isNone;
13252
+ await chat.say(state({ bot }));
13253
+ return true;
13254
+ }
13255
+ /**
13256
+ * The first @mention in a chat that has no language: the card that asks its members which, once (the chat is recorded as asked
13257
+ * before the card goes, so two mentions at once put one; a card that could not be posted is asked again next time).
13258
+ */
13259
+ async askOnce(chat) {
13260
+ const { store, logger, config } = this.ctx.deps;
13261
+ if (!store.groupLanguage.markGroupLanguageAsked(chat.scope)) return;
13262
+ const buttons2 = LANGS2.map(
13263
+ (lang) => viewButtonOf({ title: GROUP_LANGUAGE_CARD.buttons[lang], verb: GROUP_LANG_VERB, data: { lang } })
13264
+ );
13265
+ try {
13266
+ await chat.send(
13267
+ noteMessage({
13268
+ title: GROUP_LANGUAGE_CARD.title,
13269
+ text: GROUP_LANGUAGE_CARD.text({ bot: config.botName }),
13270
+ tone: "accent",
13271
+ buttons: buttons2
13272
+ })
13273
+ );
13274
+ } catch (error) {
13275
+ store.groupLanguage.unmarkGroupLanguageAsked(chat.scope);
13276
+ logger.warn("group: the language card could not be posted; it is asked again next time", {
13277
+ error: errorName(error)
13278
+ });
13279
+ }
13280
+ }
13281
+ /** A pick on the language card, by anyone in the chat: the language is set, the card says so, and so does the chat. */
13282
+ async click(action, chat) {
13283
+ const lang = action.data["lang"];
13284
+ if (!isGroupLanguage(lang)) {
13285
+ await this.ctx.settle(
13286
+ action,
13287
+ chat,
13288
+ groupLanguageText.in(chat.lang()).usage({ bot: this.ctx.deps.config.botName })
13289
+ );
13290
+ return;
13291
+ }
13292
+ this.ctx.deps.store.groupLanguage.setGroupLanguage(chat.scope, lang);
13293
+ await this.ctx.settle(action, chat, groupLanguageSet(lang, chat.sender.name));
13294
+ }
13295
+ };
13296
+
13073
13297
  // packages/core/src/routing/show-here.ts
13074
13298
  var ShowHere = class {
13075
13299
  constructor(ctx) {
@@ -13827,10 +14051,14 @@ var TurnStarter = class {
13827
14051
  review: review?.fields
13828
14052
  };
13829
14053
  }
13830
- /** The language the sender chose to be answered in, for a chat turn (a review's answer follows its request's language). */
14054
+ /**
14055
+ * The language a chat turn is to be answered in, when one was chosen (a review's answer follows its request's language): in a
14056
+ * group the group's (`groupChosenLang`; the sender's own choice is not read there), in a 1:1 chat the person's.
14057
+ */
13831
14058
  replyLang(request) {
13832
14059
  if (request.review !== void 0) return {};
13833
- const lang = chosenLang(this.ctx.deps.store.prefs.getPrefs(request.chat.sender.id));
14060
+ const { store } = this.ctx.deps;
14061
+ const lang = request.chat.scope ? groupChosenLang(store, request.chat.scope) : chosenLang(store.prefs.getPrefs(request.chat.sender.id));
13834
14062
  return lang === void 0 ? {} : { replyLang: lang };
13835
14063
  }
13836
14064
  hooks(request, run, turn, turnId) {
@@ -14176,13 +14404,14 @@ var STOP_SAID = {
14176
14404
  item_closed: ["\u5DF2\u7ECF\u7ED3\u675F\u4E86\u3002", "it had already ended."]
14177
14405
  };
14178
14406
  var GroupRouter = class {
14179
- constructor(ctx, text8, sessions, onboarding, remote, starter) {
14407
+ constructor(ctx, text8, sessions, onboarding, remote, starter, languages) {
14180
14408
  this.ctx = ctx;
14181
14409
  this.text = text8;
14182
14410
  this.sessions = sessions;
14183
14411
  this.onboarding = onboarding;
14184
14412
  this.remote = remote;
14185
14413
  this.starter = starter;
14414
+ this.languages = languages;
14186
14415
  }
14187
14416
  ctx;
14188
14417
  text;
@@ -14190,6 +14419,7 @@ var GroupRouter = class {
14190
14419
  onboarding;
14191
14420
  remote;
14192
14421
  starter;
14422
+ languages;
14193
14423
  async route(event, chat) {
14194
14424
  if (!chat.scope) return;
14195
14425
  if (event.type === "action") await this.click(event, chat);
@@ -14203,6 +14433,10 @@ var GroupRouter = class {
14203
14433
  await this.onboarding.click(action, chat);
14204
14434
  return;
14205
14435
  }
14436
+ if (action.verb === GROUP_LANG_VERB) {
14437
+ await this.languages.click(action, chat);
14438
+ return;
14439
+ }
14206
14440
  const handler = this.ctx.deps.handlers.clicks.find((h) => h.verbs.includes(action.verb));
14207
14441
  if (handler?.inGroup !== void 0) await handler.inGroup(this.ctx.clickRoute(action, chat));
14208
14442
  }
@@ -14215,6 +14449,9 @@ var GroupRouter = class {
14215
14449
  const words3 = this.words(message);
14216
14450
  learnLang(this.ctx.deps.store, chat.sender.id, message.text);
14217
14451
  const choice = message.choice === true;
14452
+ const language = !choice && await this.languages.command(chat, words3.text);
14453
+ await this.languages.askOnce(chat);
14454
+ if (language) return;
14218
14455
  if (!choice && await this.groupOwn(message, chat, words3)) return;
14219
14456
  const route = {
14220
14457
  sender: chat.sender,
@@ -14244,7 +14481,7 @@ var GroupRouter = class {
14244
14481
  const [first = ""] = pySplit(words3.text);
14245
14482
  const word2 = pyLower(first);
14246
14483
  if (empty || word2 === "/help") {
14247
- const lang = helpLang(this.ctx.deps.store, chat.sender.id, chat.locale, message.text);
14484
+ const lang = chat.lang();
14248
14485
  const assistant = this.ctx.deps.assistant.on(chat.sender.id);
14249
14486
  const groupContext = this.ctx.deps.groupHistory.kept(message.conversation.id);
14250
14487
  await chat.say(
@@ -14259,11 +14496,11 @@ var GroupRouter = class {
14259
14496
  return true;
14260
14497
  }
14261
14498
  if (github !== void 0 && word2 === "/github") {
14262
- await chat.say(groupText.in("en").githubOneToOne);
14499
+ await chat.say(groupText.in(chat.lang()).githubOneToOne);
14263
14500
  return true;
14264
14501
  }
14265
14502
  if (!this.oneToOneOnly(word2, words3.text, remote.enabled)) return false;
14266
- await chat.say(groupText.in("en").oneToOneOnly({ word: word2 }));
14503
+ await chat.say(groupText.in(chat.lang()).oneToOneOnly({ word: word2 }));
14267
14504
  return true;
14268
14505
  }
14269
14506
  /** A command of a 1:1 chat (`/agent attach|detach` binds the group: it stays the group's). */
@@ -14330,7 +14567,7 @@ ${quote}` : history || quote;
14330
14567
  };
14331
14568
  }
14332
14569
  async stopReviews(chat, pulls, text8) {
14333
- const zh = hasCjk2(text8);
14570
+ const zh = (groupChosenLang(this.ctx.deps.store, chat.scope) ?? (hasCjk2(text8) ? "zh" : "en")) === "zh";
14334
14571
  const said2 = groupText.in(zh ? "zh" : "en");
14335
14572
  const key = await this.ctx.credentials.credential(chat, false);
14336
14573
  let items;
@@ -14500,7 +14737,8 @@ var Router = class _Router {
14500
14737
  sessions,
14501
14738
  this.onboarding,
14502
14739
  new GroupRemote(this.ctx, this.text),
14503
- this.starter
14740
+ this.starter,
14741
+ new GroupLanguages(this.ctx)
14504
14742
  );
14505
14743
  }
14506
14744
  async handle(event) {
@@ -14635,7 +14873,7 @@ var Router = class _Router {
14635
14873
  };
14636
14874
 
14637
14875
  // packages/core/src/approvals/sensitive.ts
14638
- var WORDS = /* @__PURE__ */ new Set([
14876
+ var WORDS2 = /* @__PURE__ */ new Set([
14639
14877
  "token",
14640
14878
  "secret",
14641
14879
  "secrets",
@@ -14740,7 +14978,7 @@ function isSensitiveName(name) {
14740
14978
  const parts2 = nameWords(String(name));
14741
14979
  if (parts2.length === 0 || COUNTERS.has(parts2.join(" "))) return false;
14742
14980
  return parts2.some((word2, i) => {
14743
- if (WORDS.has(word2)) return true;
14981
+ if (WORDS2.has(word2)) return true;
14744
14982
  const next = parts2[i + 1];
14745
14983
  if (next === void 0) return false;
14746
14984
  const pair = `${word2} ${next}`;
@@ -19519,7 +19757,11 @@ var terminalApprovalText = defineCatalog({
19519
19757
  newText: "With:",
19520
19758
  alwaysOption: (p) => `"Always allow (this session)" means: ${p.name}`,
19521
19759
  firstAnswerWins: "The terminal is asking too: the first answer, here or there, counts.",
19760
+ afterCard: "The terminal asks only after this card is answered or its wait is over: decline it here, or hand it back to the terminal and approve it there.",
19761
+ onInput: "To answer at the terminal instead, type anything there: the question goes back to it and this card is withdrawn.",
19522
19762
  tooLong: "Too long to show in full here: it can only be turned down from the phone; approve it at the terminal.",
19763
+ tooLongAfterCard: "Too long to show in full here: it can only be declined from the phone; the terminal asks once this card's wait is over.",
19764
+ tooLongOnInput: "Too long to show in full here: it can only be turned down from the phone; to approve it, type anything at the terminal and answer it there.",
19523
19765
  questionOf: (p) => `Question ${String(p.n)} of ${String(p.of)}`,
19524
19766
  optionYes: "Yes",
19525
19767
  optionSession: "Always allow (this session)",
@@ -19527,6 +19769,7 @@ var terminalApprovalText = defineCatalog({
19527
19769
  optionNoStop: "No, and stop",
19528
19770
  optionPlanApprove: "Approve (ask before edits)",
19529
19771
  optionPlanKeep: "Keep planning",
19772
+ optionToTerminal: "Ask me at the terminal",
19530
19773
  answeredInTerminal: "Answered in the terminal",
19531
19774
  noLongerWaiting: "No longer waiting for an answer",
19532
19775
  answeredInTerminalLine: "This request was answered in the terminal.",
@@ -19550,7 +19793,11 @@ var terminalApprovalText = defineCatalog({
19550
19793
  newText: "\u6539\u4E3A\uFF1A",
19551
19794
  alwaysOption: (p) => `\u201C\u672C\u4F1A\u8BDD\u59CB\u7EC8\u5141\u8BB8\u201D\u5373\uFF1A${p.name}`,
19552
19795
  firstAnswerWins: "\u7EC8\u7AEF\u91CC\u4E5F\u5728\u7B49\u4F60\u56DE\u7B54\uFF0C\u5148\u7B54\u7684\u7B97\u6570\u3002",
19796
+ afterCard: "\u7EC8\u7AEF\u8981\u7B49\u8FD9\u5F20\u5361\u7247\u7B54\u5B8C\u6216\u7B49\u5F85\u65F6\u95F4\u5230\u4E86\u624D\u4F1A\u95EE\uFF1A\u5728\u8FD9\u91CC\u62D2\u7EDD\uFF0C\u6216\u4EA4\u56DE\u7EC8\u7AEF\u3001\u5230\u7EC8\u7AEF\u91CC\u6279\u51C6\u3002",
19797
+ onInput: "\u60F3\u6539\u5728\u7EC8\u7AEF\u91CC\u56DE\u7B54\uFF0C\u5C31\u5728\u7EC8\u7AEF\u91CC\u968F\u4FBF\u8F93\u5165\u70B9\u4EC0\u4E48\uFF1A\u95EE\u9898\u56DE\u5230\u7EC8\u7AEF\uFF0C\u8FD9\u5F20\u5361\u7247\u968F\u4E4B\u64A4\u56DE\u3002",
19553
19798
  tooLong: "\u5185\u5BB9\u8FC7\u957F\uFF0C\u8FD9\u91CC\u663E\u793A\u4E0D\u5168\uFF1A\u624B\u673A\u4E0A\u53EA\u80FD\u62D2\u7EDD\uFF0C\u8BF7\u5230\u7EC8\u7AEF\u6279\u51C6\u3002",
19799
+ tooLongAfterCard: "\u5185\u5BB9\u8FC7\u957F\uFF0C\u8FD9\u91CC\u663E\u793A\u4E0D\u5168\uFF1A\u624B\u673A\u4E0A\u53EA\u80FD\u62D2\u7EDD\uFF1B\u8FD9\u5F20\u5361\u7247\u7684\u7B49\u5F85\u65F6\u95F4\u4E00\u8FC7\uFF0C\u7EC8\u7AEF\u5C31\u4F1A\u95EE\u4F60\u3002",
19800
+ tooLongOnInput: "\u5185\u5BB9\u8FC7\u957F\uFF0C\u8FD9\u91CC\u663E\u793A\u4E0D\u5168\uFF1A\u624B\u673A\u4E0A\u53EA\u80FD\u62D2\u7EDD\uFF1B\u8981\u6279\u51C6\uFF0C\u5728\u7EC8\u7AEF\u91CC\u968F\u4FBF\u8F93\u5165\u70B9\u4EC0\u4E48\uFF0C\u5230\u7EC8\u7AEF\u91CC\u56DE\u7B54\u3002",
19554
19801
  questionOf: (p) => `\u7B2C ${String(p.n)} \u9898\uFF0C\u5171 ${String(p.of)} \u9898`,
19555
19802
  optionYes: "\u5141\u8BB8",
19556
19803
  optionSession: "\u672C\u4F1A\u8BDD\u59CB\u7EC8\u5141\u8BB8",
@@ -19558,6 +19805,7 @@ var terminalApprovalText = defineCatalog({
19558
19805
  optionNoStop: "\u62D2\u7EDD\u5E76\u505C\u6B62",
19559
19806
  optionPlanApprove: "\u6279\u51C6\uFF08\u7F16\u8F91\u524D\u9010\u4E2A\u8BE2\u95EE\uFF09",
19560
19807
  optionPlanKeep: "\u7EE7\u7EED\u89C4\u5212",
19808
+ optionToTerminal: "\u4EA4\u56DE\u7EC8\u7AEF",
19561
19809
  answeredInTerminal: "\u5DF2\u5728\u7EC8\u7AEF\u91CC\u56DE\u7B54",
19562
19810
  noLongerWaiting: "\u4E0D\u518D\u7B49\u5F85\u56DE\u7B54",
19563
19811
  answeredInTerminalLine: "\u8FD9\u4E2A\u8BF7\u6C42\u5DF2\u5728\u7EC8\u7AEF\u91CC\u56DE\u7B54\u3002",
@@ -19689,6 +19937,9 @@ var TERMINAL_FALLBACK_BYTES = 4e3;
19689
19937
  var LINE_MAX = 400;
19690
19938
  var LABEL_MAX2 = 200;
19691
19939
  var OPTION_NAME_MAX = 200;
19940
+ function terminalAsksOf(ask3) {
19941
+ return TERMINAL_ASKS.find((t4) => t4 === ask3["terminal_asks"]);
19942
+ }
19692
19943
  function isTerminalAsk(ask3) {
19693
19944
  return ask3 !== void 0 && ask3["origin"] === "terminal";
19694
19945
  }
@@ -19711,7 +19962,8 @@ function optionTitle(option, lang) {
19711
19962
  ["deny", words3.optionNo],
19712
19963
  ["deny-stop", words3.optionNoStop],
19713
19964
  ["plan-approve", words3.optionPlanApprove],
19714
- ["plan-keep", words3.optionPlanKeep]
19965
+ ["plan-keep", words3.optionPlanKeep],
19966
+ ["terminal", words3.optionToTerminal]
19715
19967
  ]);
19716
19968
  return titles.get(option.optionId);
19717
19969
  }
@@ -19830,10 +20082,15 @@ function notesOf(ask3, complete, options, words3) {
19830
20082
  const index = Number(ask3["question_index"]);
19831
20083
  if (Number.isInteger(total) && total > 1 && Number.isInteger(index))
19832
20084
  notes.push(words3.questionOf({ n: index + 1, of: total }));
19833
- if (!complete) notes.push(words3.tooLong);
20085
+ const asks = terminalAsksOf(ask3);
20086
+ if (!complete)
20087
+ notes.push(
20088
+ asks === "after_card" ? words3.tooLongAfterCard : asks === "on_input" ? words3.tooLongOnInput : words3.tooLong
20089
+ );
19834
20090
  for (const o of shownOptions(options))
19835
20091
  if (o.kind === "allow_always") notes.push(words3.alwaysOption({ name: lit(o.name, OPTION_NAME_MAX) }));
19836
- notes.push(words3.firstAnswerWins);
20092
+ if (asks === void 0) notes.push(words3.firstAnswerWins);
20093
+ else if (complete) notes.push(asks === "after_card" ? words3.afterCard : words3.onInput);
19837
20094
  return notes;
19838
20095
  }
19839
20096
  function terminalAskView(input, lang = "en") {
@@ -22239,9 +22496,9 @@ var SUMMARY_CHARS = 1200;
22239
22496
  var DM_SUMMARY_CHARS = 700;
22240
22497
  var VERDICT = pyRegExp(String.raw`^\s*VERDICT:\s*(CLEAN|BLOCKING|NOT_REVIEWED)\s*$`, "i");
22241
22498
  var CJK5 = /[一-鿿]/u;
22242
- var NAMES2 = Object.keys(PLAIN_WORDS).sort((a, b3) => b3.length - a.length).map((n) => n.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("|");
22499
+ var NAMES3 = Object.keys(PLAIN_WORDS).sort((a, b3) => b3.length - a.length).map((n) => n.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("|");
22243
22500
  var RAW_NAME = new RegExp(
22244
- `\`(?<![A-Za-z0-9_])(${NAMES2})(?![A-Za-z0-9_])\`|(?<![A-Za-z0-9_])(${NAMES2})(?![A-Za-z0-9_])`,
22501
+ `\`(?<![A-Za-z0-9_])(${NAMES3})(?![A-Za-z0-9_])\`|(?<![A-Za-z0-9_])(${NAMES3})(?![A-Za-z0-9_])`,
22245
22502
  "gu"
22246
22503
  );
22247
22504
  function plainWords(text8) {
@@ -26350,6 +26607,7 @@ __export(remote_view_exports, {
26350
26607
  statusView: () => statusView,
26351
26608
  takeoverView: () => takeoverView,
26352
26609
  terminalAskView: () => terminalAskView,
26610
+ terminalAsksOf: () => terminalAsksOf,
26353
26611
  windowView: () => windowView
26354
26612
  });
26355
26613
 
@@ -28465,8 +28723,8 @@ var STATUS_REFUSALS = /* @__PURE__ */ new Map([
28465
28723
  [403, "keyRejected"]
28466
28724
  ]);
28467
28725
  var ERROR_CODE = /^[a-z][a-z0-9_]{0,59}$/u;
28468
- function saidIn(lang, sentence2) {
28469
- return remoteAgentText.in(lang)[sentence2];
28726
+ function saidIn(lang, sentence3) {
28727
+ return remoteAgentText.in(lang)[sentence3];
28470
28728
  }
28471
28729
  function failureText2(err2, errors, now, lang = "en") {
28472
28730
  if (err2 instanceof Reconnecting) return offlineText(err2.connector, now, lang);
@@ -28705,9 +28963,9 @@ var Targets = class {
28705
28963
  */
28706
28964
  takeoverOf(chatKey, connector, agent, data, now, lang) {
28707
28965
  const entry = resumableOf(agent).find((e) => e.ref === textOf6(data["resume"]));
28708
- const no = (sentence2) => ({
28966
+ const no = (sentence3) => ({
28709
28967
  entry: null,
28710
- problem: saidIn(lang, sentence2)
28968
+ problem: saidIn(lang, sentence3)
28711
28969
  });
28712
28970
  if (entry === void 0) return no("conversationGone");
28713
28971
  if (entry.state === WATCH_STATE && data["takeover"]) {
@@ -28728,9 +28986,9 @@ var Targets = class {
28728
28986
  * process kept when it was typed (the card carries only a token).
28729
28987
  */
28730
28988
  folderOf(chatKey, agent, data, now, lang) {
28731
- const no = (sentence2) => ({
28989
+ const no = (sentence3) => ({
28732
28990
  path: "",
28733
- problem: saidIn(lang, sentence2)
28991
+ problem: saidIn(lang, sentence3)
28734
28992
  });
28735
28993
  if ("typed" in data) {
28736
28994
  const path = this.chats.typedPath(chatKey, textOf6(data["typed"]), now);
@@ -29541,7 +29799,7 @@ function older(version, than) {
29541
29799
  function isCode(reason) {
29542
29800
  return CODE4.test(reason);
29543
29801
  }
29544
- function sentence(code3, lang, setup) {
29802
+ function sentence2(code3, lang, setup) {
29545
29803
  const text8 = own(connectorReasonText.in(lang), code3) ?? "";
29546
29804
  return code3 === "revoked" ? text8.replaceAll("{login}", setup.login()) : text8;
29547
29805
  }
@@ -29565,7 +29823,7 @@ function explain(code3, version = null, setup = new Setup("", null)) {
29565
29823
  if (!KNOWN.has(code3)) return both((lang) => connectorReasonHints.in(lang).unknown({ code: code3 }));
29566
29824
  return both((lang) => {
29567
29825
  const after = hintAfter(code3, lang, version);
29568
- const own3 = sentence(code3, lang, setup);
29826
+ const own3 = sentence2(code3, lang, setup);
29569
29827
  return after ? `${own3}${after.sep}${after.hint}` : own3;
29570
29828
  });
29571
29829
  }
@@ -33198,6 +33456,7 @@ export {
33198
33456
  TAG,
33199
33457
  TARGET_LINES,
33200
33458
  TARGET_MAX,
33459
+ TERMINAL_ASKS,
33201
33460
  TERMINAL_HOOK_COMMANDS,
33202
33461
  TERMINAL_SESSION_LIMITS,
33203
33462
  THUMB_COMMENT_MAX,
@@ -33314,8 +33573,10 @@ export {
33314
33573
  githubConfirmButtons,
33315
33574
  githubConfirmMessage,
33316
33575
  githubConnectMessage,
33576
+ groupChosenLang,
33317
33577
  groupContextLines,
33318
33578
  groupContextText,
33579
+ groupLangOf,
33319
33580
  guardrailsText,
33320
33581
  hasCjk2 as hasCjk,
33321
33582
  hasControl,
@@ -4,8 +4,9 @@
4
4
  *
5
5
  * Only a choice the person said counts: a language learnt from what they write (`learnLang`), and `/lang auto`, are no
6
6
  * choice, nothing is sent, and Claw reads the language from the message as before. To the assistant the choice goes in the
7
- * message's `channel_context` (`reply_language`); to a plain session (a group's turn, or a 1:1 chat without the assistant) as
8
- * the line Claw's assistant uses for it, after the message (`replyLanguageLine`).
7
+ * message's `channel_context` (`reply_language`); to a plain session (a 1:1 chat without the assistant) as the line Claw's
8
+ * assistant uses for it, after the message (`replyLanguageLine`). A group's turn carries the group's language in that line
9
+ * instead (`groupChosenLang`): in a group a person's own choice is not read.
9
10
  */
10
11
  import type { Lang } from "../locales/lang.js";
11
12
  /** What of a person's preferences says the choice (`Prefs.lang`, `Prefs.langSet`). */
@@ -0,0 +1,18 @@
1
+ /**
2
+ * What a group chat says to set the language it is answered in: `/lang zh|en|auto` (`/lang` alone asks which it is), or one of a
3
+ * few sentences that name the group ("这个群用英文", "本群跟随提问者的语言", "use English in this group", "set the group language to
4
+ * Chinese"). Only the whole message counts. A sentence that does not name the group ("用英文回复", "answer in English") is no
5
+ * setting: it asks for that one answer in that language, and goes to the agent as it is.
6
+ */
7
+ import type { GroupLanguage } from "../ports/store/group-language.js";
8
+ /** A group's language command: set it, say which it is, or how to write it. */
9
+ export type GroupLangCommand = {
10
+ readonly kind: "set";
11
+ readonly lang: GroupLanguage;
12
+ } | {
13
+ readonly kind: "status";
14
+ } | {
15
+ readonly kind: "usage";
16
+ };
17
+ /** The group's language command `text` is (`/lang …` or a sentence that sets it), or undefined when it is none. */
18
+ export declare function parseGroupLang(text: string): GroupLangCommand | undefined;
@@ -0,0 +1,32 @@
1
+ import type { GroupLanguage } from "../ports/store/group-language.js";
2
+ /** The announcement, in the chat, of a change of its language by `name` ("" when the channel gave none). */
3
+ export declare function groupLanguageSet(lang: GroupLanguage, name: string): string;
4
+ /** The card the first @mention in a chat with no language puts to its members. */
5
+ export declare const GROUP_LANGUAGE_CARD: {
6
+ title: string;
7
+ text: (p: {
8
+ bot: string;
9
+ }) => string;
10
+ buttons: {
11
+ zh: string;
12
+ en: string;
13
+ auto: string;
14
+ };
15
+ };
16
+ export declare const groupLanguageText: import("./catalog.js").Catalog<{
17
+ isZh: (p: {
18
+ bot: string;
19
+ }) => string;
20
+ isEn: (p: {
21
+ bot: string;
22
+ }) => string;
23
+ isAuto: (p: {
24
+ bot: string;
25
+ }) => string;
26
+ isNone: (p: {
27
+ bot: string;
28
+ }) => string;
29
+ usage: (p: {
30
+ bot: string;
31
+ }) => string;
32
+ }>;
@@ -1,4 +1,4 @@
1
- /** What a group chat is told. Most of it is English only, as in the Python worker. */
1
+ /** What a group chat is told, in the group's language (`Chat.lang`). */
2
2
  export declare const groupText: import("./catalog.js").Catalog<{
3
3
  githubOneToOne: string;
4
4
  oneToOneOnly: (p: {
@@ -20,7 +20,11 @@ export declare const terminalApprovalText: import("./catalog.js").Catalog<{
20
20
  name: string;
21
21
  }) => string;
22
22
  firstAnswerWins: string;
23
+ afterCard: string;
24
+ onInput: string;
23
25
  tooLong: string;
26
+ tooLongAfterCard: string;
27
+ tooLongOnInput: string;
24
28
  questionOf: (p: {
25
29
  n: number;
26
30
  of: number;
@@ -31,6 +35,7 @@ export declare const terminalApprovalText: import("./catalog.js").Catalog<{
31
35
  optionNoStop: string;
32
36
  optionPlanApprove: string;
33
37
  optionPlanKeep: string;
38
+ optionToTerminal: string;
34
39
  answeredInTerminal: string;
35
40
  noLongerWaiting: string;
36
41
  answeredInTerminalLine: string;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * The language each group chat is answered in, as its members set it (`@bot /lang en|zh|auto`, "这个群用英文", or a pick on the
3
+ * card the bot posts the first time it is @mentioned there). `auto`: each question is answered in the language it is asked in.
4
+ * A chat the card was put to and nobody answered yet is kept as asked (`lang` ""), so it is never asked again. Who set it is
5
+ * never stored: the announcement names them once, in the chat.
6
+ */
7
+ export type GroupLanguage = "zh" | "en" | "auto";
8
+ export interface GroupLanguageRepository {
9
+ /** The chat's language; "" when it was asked and nobody chose; null when it was never asked nor set. */
10
+ groupLanguage(conversationId: string): GroupLanguage | "" | null;
11
+ /** Sets it (anyone in the chat may). */
12
+ setGroupLanguage(conversationId: string, lang: GroupLanguage): void;
13
+ /**
14
+ * Records that the chat is asked now: true when this call recorded it (it was never asked nor set), false when it was already, in
15
+ * one step, so that two @mentions at once put one card.
16
+ */
17
+ markGroupLanguageAsked(conversationId: string): boolean;
18
+ /** The question could not be put after all: the chat is asked again next time (nothing is undone once somebody chose). */
19
+ unmarkGroupLanguageAsked(conversationId: string): void;
20
+ }
@@ -14,6 +14,7 @@ import type { AssistantButtonRepository, AssistantCardRepository, AssistantMessa
14
14
  import type { FeedbackRepository } from "./feedback.js";
15
15
  import type { GithubConfirmationRepository, ReviewDelegationRepository } from "./github.js";
16
16
  import type { GroupContextRepository } from "./group-context.js";
17
+ import type { GroupLanguageRepository } from "./group-language.js";
17
18
  import type { IdempotencyRepository } from "./idempotency.js";
18
19
  import type { LinkRepository } from "./link.js";
19
20
  import type { NotifySubscriptionRepository } from "./notify.js";
@@ -33,6 +34,7 @@ export interface StorePort {
33
34
  readonly approvals: ApprovalRepository;
34
35
  readonly observe: ObserveRepository;
35
36
  readonly groupContext: GroupContextRepository;
37
+ readonly groupLanguage: GroupLanguageRepository;
36
38
  readonly assistantCards: AssistantCardRepository;
37
39
  readonly assistantButtons: AssistantButtonRepository;
38
40
  readonly assistantMessages: AssistantMessageRepository;
@@ -55,6 +57,7 @@ export type * from "./assistant.js";
55
57
  export type * from "./feedback.js";
56
58
  export type * from "./github.js";
57
59
  export type * from "./group-context.js";
60
+ export type * from "./group-language.js";
58
61
  export type * from "./idempotency.js";
59
62
  export type * from "./link.js";
60
63
  export type * from "./notify.js";
@@ -121,8 +121,8 @@ export interface AssistantRemoteAsk {
121
121
  /** When the connector stops waiting for an answer (it refuses the request then); absent when it did not say. */
122
122
  expires_at?: string;
123
123
  /**
124
- * `terminal`: asked in a terminal on the machine (a conversation its person started there), whose own prompt stays open: whichever
125
- * answer comes first is taken. Absent for a session opened through Claw.
124
+ * `terminal`: asked in a terminal on the machine (a conversation its person started there), which asks it too (when: see
125
+ * `terminal_asks`); whichever answer comes first is taken. Absent for a session opened through Claw.
126
126
  */
127
127
  origin?: "terminal";
128
128
  /** A terminal's request: what kind of prompt it is (`execute`, `edit`, `question`, `switch_mode` for a plan, `mcp`, `other`). */
@@ -139,7 +139,18 @@ export interface AssistantRemoteAsk {
139
139
  /** A terminal's question asked as several cards: which this is (from 0), and how many. */
140
140
  question_index?: number;
141
141
  question_count?: number;
142
+ /**
143
+ * A terminal's request: when the terminal asks it too, as its connector said (`terminal_asks` on the request). Absent: alongside
144
+ * the card, and the first answer counts (Claude Code). `after_card`: only once the card is answered or its time is up, and the
145
+ * card offers only to decline or to hand it back to the terminal (Cursor). `on_input`: only once someone types in that terminal,
146
+ * which withdraws the card (Codex).
147
+ */
148
+ terminal_asks?: TerminalAsks;
142
149
  }
150
+ /** When a terminal asks a request that is also on a card, besides alongside it: see {@link AssistantRemoteAsk.terminal_asks}. */
151
+ export type TerminalAsks = "after_card" | "on_input";
152
+ /** The values of `terminal_asks` a channel knows; any other is read as absent. */
153
+ export declare const TERMINAL_ASKS: readonly TerminalAsks[];
143
154
  /** A choice a say puts to the person: from 2 to 10 options, one plain line each; `multiple` when several may be picked. */
144
155
  export interface AssistantSayChoice {
145
156
  options: string[];
@@ -1,7 +1,9 @@
1
1
  /**
2
2
  * The card of a question a terminal asks: a coding agent its person started in their own terminal wants to run a command, change
3
3
  * a file, start on a plan or have a multiple-choice question answered, and the connector's approval hooks put it here too
4
- * (`remote.ask` with `origin: "terminal"`). The terminal keeps asking: the first answer counts.
4
+ * (`remote.ask` with `origin: "terminal"`). The terminal asks too, as the request's `terminal_asks` says (`terminalAsksOf`): alongside
5
+ * the card, the first answer counting (Claude Code); only once the card is answered or its wait is over (Cursor); or once someone
6
+ * types in that terminal, which takes the question back from the card (Codex). The card says which.
5
7
  *
6
8
  * Nothing is approved unseen. The card shows the request whole (the connector and Claw carry it whole, credentials masked); a
7
9
  * request whose content did not come (Claw leaves it out past its size), or that is longer than a card shows, keeps only the
@@ -9,6 +11,7 @@
9
11
  * masked in it (a mask can hide more than the credential), or a character the card does not show as itself.
10
12
  */
11
13
  import type { Lang } from "../locales/lang.js";
14
+ import { type TerminalAsks } from "../protocol.js";
12
15
  import type { DecisionButton } from "../remote/actions.js";
13
16
  import { type PermissionOption } from "../remote/options.js";
14
17
  import type { Reported } from "../remote/reported.js";
@@ -62,6 +65,13 @@ export interface TerminalAsk {
62
65
  readonly machine: string;
63
66
  readonly agent: string;
64
67
  }
68
+ /**
69
+ * When the terminal asks a request that is also on a card (`remote.ask`'s `terminal_asks`, from the connector through Claw):
70
+ * `after_card`, only once the card is answered or its wait is over, and the card offers only to decline or hand it back (Cursor);
71
+ * `on_input`, only once someone types in that terminal, which withdraws the card (Codex); undefined (absent, or a value this
72
+ * client does not know), alongside the card, the first answer counting (Claude Code).
73
+ */
74
+ export declare function terminalAsksOf(ask: Reported): TerminalAsks | undefined;
65
75
  /** Whether `remote.ask` is a question a terminal asks (a connector's approval hooks), not one an agent Claw started asks. */
66
76
  export declare function isTerminalAsk(ask: Reported | undefined): boolean;
67
77
  /** The card of a question a terminal asks. */
@@ -19,6 +19,15 @@ export declare function learnLang(store: Pick<RouterStore, "prefs">, userId: str
19
19
  * "what can you do") and the person did not choose one (`/lang`).
20
20
  */
21
21
  export declare function helpLang(store: Pick<RouterStore, "prefs">, userId: string, locale: string, asked: string | undefined): Lang;
22
+ /**
23
+ * The language a group chat is answered in, its fixed texts and its answers alike: the one its members set (`/lang zh|en` there,
24
+ * or a pick on its language card); else (none set, or "follow the asker") the language of the message being answered (`written`).
25
+ * A person's own choice (`/lang` in their 1:1 chat) is not read here: in a group the group decides. A message that says no
26
+ * language of its own ("/new", a link) is answered in the one the sender's messages were last written in, else their client's.
27
+ */
28
+ export declare function groupLangOf(store: Pick<RouterStore, "prefs"> & Partial<Pick<RouterStore, "groupLanguage">>, conversationId: string, userId: string, locale: string, written: string): Lang;
29
+ /** The language a group chat's members set, when it is one (`/lang zh|en`); undefined for none, and for "follow the asker". */
30
+ export declare function groupChosenLang(store: Partial<Pick<RouterStore, "groupLanguage">>, conversationId: string): Lang | undefined;
22
31
  /** One chat as the router answers in it: who wrote, where to answer, and in which language. */
23
32
  export declare class Chat {
24
33
  private readonly store;
@@ -28,11 +37,16 @@ export declare class Chat {
28
37
  readonly scope: string;
29
38
  /** The client's locale of the event being handled ("" for work that runs on its own). */
30
39
  readonly locale: string;
31
- constructor(store: Pick<RouterStore, "prefs">, sender: UserRef, conversation: Conversation,
40
+ /** The words of the message being answered ("" for a click, and for work that runs on its own): a group's language without one. */
41
+ readonly written: string;
42
+ constructor(store: Pick<RouterStore, "prefs"> & Partial<Pick<RouterStore, "groupLanguage">>, sender: UserRef, conversation: Conversation,
32
43
  /** "" for the person's 1:1 chat, the conversation id of a group. */
33
44
  scope: string,
34
45
  /** The client's locale of the event being handled ("" for work that runs on its own). */
35
- locale: string);
46
+ locale: string,
47
+ /** The words of the message being answered ("" for a click, and for work that runs on its own): a group's language without one. */
48
+ written?: string);
49
+ /** In a 1:1 chat the person's language (`langOf`); in a group, the group's (`groupLangOf`). */
36
50
  lang(): Lang;
37
51
  zh(): boolean;
38
52
  say(text: string): Promise<void>;
@@ -38,7 +38,7 @@ export interface RouterConfig {
38
38
  readonly ownThumbsMobile: boolean;
39
39
  }
40
40
  /** The repositories the router reads and writes (the W1 store, as far as routing goes). */
41
- export type RouterStore = Pick<StorePort, "links" | "references" | "prefs" | "members" | "sessions" | "repoHistory" | "turns" | "onboarding" | "idempotency" | "assistantMessages" | "notifySubs" | "reviewDelegation" | "approvals" | "now">;
41
+ export type RouterStore = Pick<StorePort, "links" | "references" | "prefs" | "groupLanguage" | "members" | "sessions" | "repoHistory" | "turns" | "onboarding" | "idempotency" | "assistantMessages" | "notifySubs" | "reviewDelegation" | "approvals" | "now">;
42
42
  /** The per-person limits (Python `limits.py`): messages per window and tasks at the same time. */
43
43
  export interface RouterLimits {
44
44
  readonly maxConcurrentTasks: number;
@@ -0,0 +1,25 @@
1
+ import type { InboundAction } from "../ports/surface.js";
2
+ import type { Chat } from "./chat.js";
3
+ import type { RouterContext } from "./context.js";
4
+ /** The verb of the language card's buttons. */
5
+ export declare const GROUP_LANG_VERB = "group_lang";
6
+ /**
7
+ * The language a group chat is answered in, as its members set it (the store's `groupLanguage`): `@bot /lang zh|en|auto` or a
8
+ * sentence that names the group ("这个群用英文"), and a card the first time the bot is @mentioned in a chat that has none. Anyone in
9
+ * the chat may set it, with no Claw credential (it is the chat's, not one person's), and every change is announced in the chat with
10
+ * the name of who made it. The group's language then governs everything said there (`Chat.lang`, `groupLangOf`) and the language
11
+ * its turns are asked to answer in (`TurnStarter`).
12
+ */
13
+ export declare class GroupLanguages {
14
+ private readonly ctx;
15
+ constructor(ctx: RouterContext);
16
+ /** `/lang …` or a sentence that sets the group's language: answered here; false when `text` is neither. */
17
+ command(chat: Chat, text: string): Promise<boolean>;
18
+ /**
19
+ * The first @mention in a chat that has no language: the card that asks its members which, once (the chat is recorded as asked
20
+ * before the card goes, so two mentions at once put one; a card that could not be posted is asked again next time).
21
+ */
22
+ askOnce(chat: Chat): Promise<void>;
23
+ /** A pick on the language card, by anyone in the chat: the language is set, the card says so, and so does the chat. */
24
+ click(action: InboundAction, chat: Chat): Promise<void>;
25
+ }
@@ -3,6 +3,7 @@ import type { InboundAction, InboundMessage } from "../ports/surface.js";
3
3
  import { type Chat } from "./chat.js";
4
4
  import type { SessionCommands } from "./commands-session.js";
5
5
  import type { RouterContext } from "./context.js";
6
+ import { type GroupLanguages } from "./group-language.js";
6
7
  import type { GroupRemote } from "./group-remote.js";
7
8
  import type { Onboarding } from "./onboarding.js";
8
9
  import { type PersonalText } from "./personal-text.js";
@@ -18,7 +19,8 @@ export declare class GroupRouter {
18
19
  private readonly onboarding;
19
20
  private readonly remote;
20
21
  private readonly starter;
21
- constructor(ctx: RouterContext, text: PersonalText, sessions: SessionCommands, onboarding: Onboarding, remote: GroupRemote, starter: TurnStarter);
22
+ private readonly languages;
23
+ constructor(ctx: RouterContext, text: PersonalText, sessions: SessionCommands, onboarding: Onboarding, remote: GroupRemote, starter: TurnStarter, languages: GroupLanguages);
22
24
  route(event: InboundMessage | InboundAction, chat: Chat): Promise<void>;
23
25
  /** A button in a group: only those of the presser's own cards that may be pressed there answer. */
24
26
  private click;
@@ -44,8 +44,9 @@ export interface TurnRun {
44
44
  readonly files: readonly InboundFile[];
45
45
  readonly lang: Lang;
46
46
  /**
47
- * The language the person chose to be answered in (`/lang zh|en`, `chosenLang`), told to Claw after the message; undefined: they
48
- * chose none, and the answer is in the language of what they wrote. A chat turn's only: a group's is the sender's.
47
+ * The language the turn is to be answered in, told to Claw after the message: in a 1:1 chat the one the person chose (`/lang
48
+ * zh|en`, `chosenLang`), in a group the group's (`groupChosenLang`); undefined: none was chosen, and the answer is in the language
49
+ * of what they wrote. A chat turn's only: a review's follows its request.
49
50
  */
50
51
  readonly replyLang?: Lang;
51
52
  /** The answer gets thumbs of the worker's own (a phone, whose own thumbs never reach the worker). */
@@ -1,7 +1,7 @@
1
1
  import type { Lang } from "../locales/lang.js";
2
2
  import type { Logger } from "../ports/logger.js";
3
3
  import type { InboundFile } from "../ports/surface.js";
4
- import type { Chat } from "./chat.js";
4
+ import { type Chat } from "./chat.js";
5
5
  import type { RouterContext } from "./context.js";
6
6
  import type { ReviewRunFields, TurnOutcome } from "./turn-runner.js";
7
7
  /** Teams on a phone or a tablet: its own Stop under a reply is greyed out, and its own thumbs never reach the worker. */
@@ -97,7 +97,10 @@ export declare class TurnStarter {
97
97
  /** What the run reads of the chat's session: its id, the model it has, the repository a new one is bound to. */
98
98
  private sessionPart;
99
99
  private runOf;
100
- /** The language the sender chose to be answered in, for a chat turn (a review's answer follows its request's language). */
100
+ /**
101
+ * The language a chat turn is to be answered in, when one was chosen (a review's answer follows its request's language): in a
102
+ * group the group's (`groupChosenLang`; the sender's own choice is not read there), in a 1:1 chat the person's.
103
+ */
101
104
  private replyLang;
102
105
  private hooks;
103
106
  private guarded;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@primusclaw/personal-agent",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "The personal agent's channel-neutral core: domain logic, ports and plugin interfaces.",
5
5
  "license": "MIT",
6
6
  "repository": {