appback-remoteagent 0.14.6 → 0.15.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.
@@ -0,0 +1,26 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ const CHILD_ENV_BLOCKED_PREFIXES = ["TELEGRAM_"];
5
+ export function buildProviderEnv(extraEnv) {
6
+ const env = { ...process.env };
7
+ for (const key of Object.keys(env)) {
8
+ if (CHILD_ENV_BLOCKED_PREFIXES.some((prefix) => key.startsWith(prefix))) {
9
+ delete env[key];
10
+ }
11
+ }
12
+ const dataDir = process.env.REMOTEAGENT_DATA_DIR?.trim()
13
+ || process.env.DATA_DIR?.trim()
14
+ || path.join(os.homedir(), ".remoteagent");
15
+ env.DATA_DIR = dataDir;
16
+ env.REMOTEAGENT_DATA_DIR = dataDir;
17
+ env.REMOTEAGENT_SECRET_BIN = resolveSecretHelperPath();
18
+ if (extraEnv) {
19
+ Object.assign(env, extraEnv);
20
+ }
21
+ return env;
22
+ }
23
+ function resolveSecretHelperPath() {
24
+ const adapterDir = path.dirname(fileURLToPath(import.meta.url));
25
+ return path.resolve(adapterDir, "..", "secret-helper.js");
26
+ }
@@ -1,6 +1,6 @@
1
- import process from "node:process";
2
1
  import { promisify } from "node:util";
3
2
  import { exec as execCallback } from "node:child_process";
3
+ import { buildProviderEnv } from "./runtime-env.js";
4
4
  const exec = promisify(execCallback);
5
5
  export class ShellAdapter {
6
6
  provider;
@@ -12,8 +12,7 @@ export class ShellAdapter {
12
12
  this.timeoutMs = timeoutMs;
13
13
  }
14
14
  async send(request) {
15
- const env = {
16
- ...process.env,
15
+ const env = buildProviderEnv({
17
16
  BRIDGE_PROVIDER: this.provider,
18
17
  BRIDGE_BOT_ID: request.botId ?? "",
19
18
  BRIDGE_CHAT_ID: request.chatId,
@@ -21,7 +20,7 @@ export class ShellAdapter {
21
20
  BRIDGE_PUBLIC_SESSION_ID: request.publicSessionId ?? "",
22
21
  BRIDGE_CWD: request.cwd,
23
22
  BRIDGE_MESSAGE: request.message,
24
- };
23
+ });
25
24
  const { stdout, stderr } = await exec(this.command, {
26
25
  env,
27
26
  timeout: this.currentTimeoutMs(),
@@ -1,34 +1,17 @@
1
1
  import process from "node:process";
2
- import path from "node:path";
3
- import os from "node:os";
4
2
  import { execFile, spawn } from "node:child_process";
3
+ import { buildProviderEnv } from "./runtime-env.js";
5
4
  const activeCommands = new Map();
6
- const CHILD_ENV_BLOCKED_PREFIXES = ["TELEGRAM_"];
7
- function buildChildEnv(extraEnv) {
8
- const env = { ...process.env };
9
- for (const key of Object.keys(env)) {
10
- if (CHILD_ENV_BLOCKED_PREFIXES.some((prefix) => key.startsWith(prefix))) {
11
- delete env[key];
12
- }
13
- }
14
- const dataDir = process.env.DATA_DIR?.trim() || path.join(os.homedir(), ".remoteagent");
15
- env.REMOTEAGENT_DATA_DIR = dataDir;
16
- env.REMOTEAGENT_SECRET_BIN = path.resolve(process.cwd(), "dist", "secret-helper.js");
17
- if (extraEnv) {
18
- Object.assign(env, extraEnv);
19
- }
20
- return env;
21
- }
22
5
  export function spawnWithPlatformShell(bin, args, cwd, timeoutMs, input, executionKey, extraEnv) {
23
6
  return new Promise((resolve, reject) => {
24
7
  const command = process.platform === "win32"
25
8
  ? spawn("cmd.exe", ["/d", "/c", "call", bin, ...args], {
26
9
  cwd,
27
- env: buildChildEnv(extraEnv),
10
+ env: buildProviderEnv(extraEnv),
28
11
  })
29
12
  : spawn(bin, args, {
30
13
  cwd,
31
- env: buildChildEnv(extraEnv),
14
+ env: buildProviderEnv(extraEnv),
32
15
  detached: true,
33
16
  });
34
17
  if (executionKey) {
package/dist/bot.js CHANGED
@@ -18,6 +18,7 @@ const HELP_TEXT = [
18
18
  "/list [-a]",
19
19
  "/new",
20
20
  "/switch <session>",
21
+ "/plan <count>",
21
22
  "/batch start|send|cancel|status",
22
23
  "/attach codex <thread_id>",
23
24
  "/attach claude <session_id>",
@@ -29,7 +30,8 @@ const HELP_TEXT = [
29
30
  "/state [clear|note <text>]",
30
31
  "/artifacts list|cleanup <days>",
31
32
  "/secret set|list|remove",
32
- "/docs pin|find|list|remove",
33
+ "/docs pin|find|list|remove|reinforce",
34
+ "/보강 <count>",
33
35
  "/bots",
34
36
  "/bot add <token>",
35
37
  "/bot doctor",
@@ -70,6 +72,7 @@ const RECOGNIZED_COMMANDS = new Set([
70
72
  "list",
71
73
  "new",
72
74
  "switch",
75
+ "plan",
73
76
  "batch",
74
77
  "attach",
75
78
  "model",
@@ -258,6 +261,19 @@ export function createBot(token, bridge, botManagement, botInfo) {
258
261
  throw error;
259
262
  }
260
263
  };
264
+ const runPlanDocumentReinforcement = async (ctx, count) => {
265
+ if (!ctx.chat) {
266
+ throw new Error("Telegram chat context is missing.");
267
+ }
268
+ const botId = getBotId();
269
+ const chatId = String(ctx.chat.id);
270
+ await bridge.logSystem(botId, chatId, `Plan document reinforcement requested (${count} turns max).`);
271
+ await runWithPendingAnimation(token, ctx.chat.id, async (helpers) => {
272
+ return {
273
+ chunks: await routeTelegramWorkLoop(bridge, botId, chatId, formatPlanDocumentReinforcementPrompt(count), "Plan document reinforcement", botManagement, helpers, autoContinue, memoryService, (blocks) => blocks, { maxTurns: count }),
274
+ };
275
+ });
276
+ };
261
277
  bot.use(async (ctx, next) => {
262
278
  const updateKind = Object.keys(ctx.update).join(",");
263
279
  const text = ctx.message?.text ?? ctx.editedMessage?.text ?? ctx.channelPost?.text ?? "";
@@ -365,6 +381,19 @@ ${bridge.formatStatus(mapping)}`);
365
381
  }
366
382
  await reply(ctx, `Switched this chat to session ${sessionId}.\n\n${bridge.formatCurrentSession(mapping)}`);
367
383
  });
384
+ bot.command("plan", async (ctx) => {
385
+ const { args, rest } = parseCommand(ctx.message?.text, 1);
386
+ if (rest?.trim()) {
387
+ await reply(ctx, "Usage: `/plan <1-10>`", { parse_mode: "Markdown" });
388
+ return;
389
+ }
390
+ const parsed = parsePlanReinforcementCount(args[0]?.trim());
391
+ if (parsed.kind === "invalid") {
392
+ await reply(ctx, "Usage: `/plan <1-10>`", { parse_mode: "Markdown" });
393
+ return;
394
+ }
395
+ await runPlanDocumentReinforcement(ctx, parsed.count);
396
+ });
368
397
  bot.command("batch", async (ctx) => {
369
398
  const botId = getBotId();
370
399
  const chatId = String(ctx.chat.id);
@@ -649,6 +678,19 @@ ${bridge.formatStatus(mapping)}`);
649
678
  const { args, rest } = parseCommand(ctx.message?.text, 2);
650
679
  const action = args[0]?.toLowerCase() || "list";
651
680
  const keyword = args[1]?.trim();
681
+ if (action === "reinforce") {
682
+ if (rest?.trim()) {
683
+ await reply(ctx, "Usage: `/docs reinforce <1-10>`", { parse_mode: "Markdown" });
684
+ return;
685
+ }
686
+ const parsed = parsePlanReinforcementCount(keyword);
687
+ if (parsed.kind === "invalid") {
688
+ await reply(ctx, "Usage: `/docs reinforce <1-10>`", { parse_mode: "Markdown" });
689
+ return;
690
+ }
691
+ await runPlanDocumentReinforcement(ctx, parsed.count);
692
+ return;
693
+ }
652
694
  if (action === "list") {
653
695
  await reply(ctx, await memoryService.listDocuments());
654
696
  return;
@@ -681,7 +723,7 @@ ${bridge.formatStatus(mapping)}`);
681
723
  await reply(ctx, removed ? `Removed docs keyword ${keyword}.` : `Docs keyword was not found: ${keyword}`);
682
724
  return;
683
725
  }
684
- await reply(ctx, "Usage: `/docs list`, `/docs find <keyword>`, `/docs pin <keyword> <path>`, or `/docs remove <keyword>`", { parse_mode: "Markdown" });
726
+ await reply(ctx, "Usage: `/docs list`, `/docs find <keyword>`, `/docs pin <keyword> <path>`, `/docs remove <keyword>`, or `/docs reinforce <1-10>`", { parse_mode: "Markdown" });
685
727
  });
686
728
  bot.command("bots", async (ctx) => {
687
729
  await ensureOwnerControlAccess(ctx);
@@ -799,6 +841,15 @@ ${bridge.formatStatus(mapping)}`);
799
841
  const voice = ctx.message.voice;
800
842
  const audio = ctx.message.audio;
801
843
  const text = ctx.message.text?.trim();
844
+ const planReinforcement = parseKoreanPlanReinforcementCommand(text, botId);
845
+ if (planReinforcement.kind === "matched") {
846
+ await runPlanDocumentReinforcement(ctx, planReinforcement.count);
847
+ return;
848
+ }
849
+ if (planReinforcement.kind === "invalid") {
850
+ await reply(ctx, "Usage: `/보강 <1-10>`", { parse_mode: "Markdown" });
851
+ return;
852
+ }
802
853
  if (text && isRecognizedSlashCommand(text, botId)) {
803
854
  return;
804
855
  }
@@ -1130,7 +1181,7 @@ async function runWithPendingAnimation(botToken, chatId, task) {
1130
1181
  }
1131
1182
  }
1132
1183
  }
1133
- async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botManagement, helpers, autoContinue, memoryService, transform = (blocks) => blocks) {
1184
+ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botManagement, helpers, autoContinue, memoryService, transform = (blocks) => blocks, options = {}) {
1134
1185
  const currentSession = await bridge.status(botId, chatId);
1135
1186
  const sessionId = currentSession?.session.sessionId;
1136
1187
  const activeKey = workLoopKey(botId, chatId, sessionId);
@@ -1151,7 +1202,7 @@ async function routeTelegramWorkLoop(bridge, botId, chatId, message, label, botM
1151
1202
  : "";
1152
1203
  autoContinue.clear(botId, chatId, sessionId);
1153
1204
  let prompt = appendManagedContext(appendReportProtocol(message), managedContext);
1154
- const maxTurns = config.telegramAutoProgressMaxTurns;
1205
+ const maxTurns = options.maxTurns ?? config.telegramAutoProgressMaxTurns;
1155
1206
  const emptyResponseRetries = config.telegramEmptyResponseRetries;
1156
1207
  const retryableErrorRetries = config.telegramRetryableErrorRetries;
1157
1208
  const retryableErrorDelayMs = config.telegramRetryableErrorDelayMs;
@@ -1683,6 +1734,57 @@ function parseCommand(text, headCount) {
1683
1734
  rest: remaining || undefined,
1684
1735
  };
1685
1736
  }
1737
+ function parsePlanReinforcementCount(raw) {
1738
+ const value = raw?.trim();
1739
+ if (!value) {
1740
+ return { kind: "valid", count: 5 };
1741
+ }
1742
+ if (!/^\d+$/.test(value)) {
1743
+ return { kind: "invalid" };
1744
+ }
1745
+ const count = Number(value);
1746
+ if (!Number.isInteger(count) || count < 1 || count > 10) {
1747
+ return { kind: "invalid" };
1748
+ }
1749
+ return { kind: "valid", count };
1750
+ }
1751
+ function parseKoreanPlanReinforcementCommand(text, botId) {
1752
+ const trimmed = text?.trim();
1753
+ if (!trimmed?.startsWith("/")) {
1754
+ return { kind: "none" };
1755
+ }
1756
+ const [commandToken, maybeCount, ...extra] = trimmed.slice(1).split(/\s+/);
1757
+ const [name, mention] = commandToken.split("@", 2);
1758
+ if (name !== "보강") {
1759
+ return { kind: "none" };
1760
+ }
1761
+ if (mention && mention.toLowerCase() !== botId.toLowerCase()) {
1762
+ return { kind: "none" };
1763
+ }
1764
+ if (extra.length > 0) {
1765
+ return { kind: "invalid" };
1766
+ }
1767
+ const parsed = parsePlanReinforcementCount(maybeCount);
1768
+ return parsed.kind === "valid" ? { kind: "matched", count: parsed.count } : { kind: "invalid" };
1769
+ }
1770
+ function formatPlanDocumentReinforcementPrompt(count) {
1771
+ return [
1772
+ `계획문서를 처음부터 끝까지 확인하고, 최대 ${count}회까지만 보강 루프를 수행하세요.`,
1773
+ "",
1774
+ "반복 절차:",
1775
+ "1. 계획문서와 관련 문서를 실제로 읽고 보강할 내용이 있는지 판단하세요.",
1776
+ "2. 보강할 내용이 있으면 직접 문서를 수정하세요.",
1777
+ "3. 수정 후 같은 기준으로 다시 점검하세요.",
1778
+ `4. 더 이상 보강할 내용이 없거나 ${count}회에 도달하면 종료하세요.`,
1779
+ "",
1780
+ "중요 규칙:",
1781
+ "- 추정하지 말고 실제 파일 경로와 확인 근거를 남기세요.",
1782
+ "- 같은 보강을 반복하지 마세요.",
1783
+ "- 보강할 내용이 없으면 없다고 보고하고 종료하세요.",
1784
+ "- 권한, 정보, 파일 위치가 부족하면 REPORT:blocked로 정확한 blocker를 말하세요.",
1785
+ "- 완료 시 확인한 문서, 변경한 문서, 남은 위험, 검증 결과를 짧게 보고하세요.",
1786
+ ].join("\n");
1787
+ }
1686
1788
  function commandTarget(args, rest) {
1687
1789
  return (rest?.trim() || args.slice(1).join(" ").trim() || args[0]?.trim() || "");
1688
1790
  }
@@ -6,6 +6,7 @@ export const TELEGRAM_COMMAND_MENU = [
6
6
  { command: "list", description: "List sessions" },
7
7
  { command: "new", description: "Start a fresh session" },
8
8
  { command: "switch", description: "Switch to a session" },
9
+ { command: "plan", description: "Reinforce planning documents" },
9
10
  { command: "status", description: "Show current session status" },
10
11
  { command: "attach", description: "Attach an existing provider session" },
11
12
  { command: "state", description: "Show or edit session state notes" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "appback-remoteagent",
3
- "version": "0.14.6",
3
+ "version": "0.15.0",
4
4
  "description": "Personal installable session server for continuing local AI work across PC and Telegram",
5
5
  "license": "MIT",
6
6
  "type": "module",