@oyasmi/pipiclaw 0.1.1 → 0.2.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.
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { parseBuiltInCommand } from \"./commands.js\";\nimport { createDingTalkContext } from \"./delivery.js\";\nimport { DingTalkBot, type DingTalkConfig, type DingTalkEvent, type DingTalkHandler } from \"./dingtalk.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport {\n\tAPP_HOME_DIR,\n\tAPP_NAME,\n\tAUTH_CONFIG_PATH,\n\tCHANNEL_CONFIG_PATH,\n\tMODELS_CONFIG_PATH,\n\tSETTINGS_CONFIG_PATH,\n\tWORKSPACE_DIR,\n} from \"./paths.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\nif (process.env.DINGTALK_FORCE_PROXY !== \"true\") {\n\tdelete process.env.http_proxy;\n\tdelete process.env.https_proxy;\n\tdelete process.env.all_proxy;\n\tdelete process.env.HTTP_PROXY;\n\tdelete process.env.HTTPS_PROXY;\n\tdelete process.env.ALL_PROXY;\n}\n\ninterface DingTalkAppConfig extends DingTalkConfig {}\n\ninterface ParsedArgs {\n\tsandbox: SandboxConfig;\n}\n\ninterface BootstrapResult {\n\tcreated: string[];\n\tchannelTemplateCreated: boolean;\n}\n\nconst DEFAULT_SOUL = `# SOUL.md\n\nConfigure Pipiclaw's identity, voice, and communication style here.\n\nSuggested sections:\n\n- Who the assistant is\n- Default language\n- Tone and personality\n- Reply style\n- Formatting preferences\n\nExample topics you may want to define:\n\n- \"Answer in Chinese by default.\"\n- \"Be concise and direct.\"\n- \"Prefer Markdown.\"\n- \"Act as an engineering assistant for our team.\"\n\nReplace this template with your actual identity prompt.\n`;\n\nconst DEFAULT_AGENT = `# AGENT.md\n\nConfigure Pipiclaw's behavioral rules and operating constraints here.\n\nSuggested sections:\n\n- Tool usage policy\n- File access rules\n- Security constraints\n- Approval rules\n- Project-specific workflows\n- Things the assistant must always or never do\n\nExample topics you may want to define:\n\n- Which tools should be preferred first\n- Whether installation commands are allowed\n- How to handle sensitive data\n- Coding conventions for your team\n- Deployment or release restrictions\n\nReplace this template with your actual operating instructions.\n`;\n\nconst DEFAULT_MEMORY = `# 工作记忆\n\n(尚无记录)\n`;\n\nconst CHANNEL_CONFIG_TEMPLATE = {\n\tclientId: \"your-dingtalk-client-id\",\n\tclientSecret: \"your-dingtalk-client-secret\",\n\trobotCode: \"your-robot-code\",\n\tcardTemplateId: \"your-card-template-id\",\n\tcardTemplateKey: \"content\",\n\tallowFrom: [\"your-staff-id\"],\n} satisfies DingTalkAppConfig;\n\nconst MODELS_CONFIG_TEMPLATE = { providers: {} };\n\nfunction writeTextFileIfMissing(path: string, content: string, label: string, created: string[]): boolean {\n\tif (existsSync(path)) {\n\t\treturn false;\n\t}\n\twriteFileSync(path, content, \"utf-8\");\n\tcreated.push(label);\n\treturn true;\n}\n\nfunction writeJsonFileIfMissing(path: string, value: unknown, label: string, created: string[]): boolean {\n\treturn writeTextFileIfMissing(path, `${JSON.stringify(value, null, 2)}\\n`, label, created);\n}\n\nfunction bootstrapAppHome(): BootstrapResult {\n\tconst created: string[] = [];\n\n\tif (!existsSync(APP_HOME_DIR)) {\n\t\tmkdirSync(APP_HOME_DIR, { recursive: true });\n\t\tcreated.push(\"app home\");\n\t}\n\tif (!existsSync(WORKSPACE_DIR)) {\n\t\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n\t\tcreated.push(\"workspace/\");\n\t}\n\n\tfor (const dir of [\"skills\", \"events\"]) {\n\t\tconst dirPath = join(WORKSPACE_DIR, dir);\n\t\tif (!existsSync(dirPath)) {\n\t\t\tmkdirSync(dirPath, { recursive: true });\n\t\t\tcreated.push(`workspace/${dir}/`);\n\t\t}\n\t}\n\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"SOUL.md\"), DEFAULT_SOUL, \"workspace/SOUL.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"AGENT.md\"), DEFAULT_AGENT, \"workspace/AGENT.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"MEMORY.md\"), DEFAULT_MEMORY, \"workspace/MEMORY.md\", created);\n\n\tconst channelTemplateCreated = writeJsonFileIfMissing(\n\t\tCHANNEL_CONFIG_PATH,\n\t\tCHANNEL_CONFIG_TEMPLATE,\n\t\t\"channel.json\",\n\t\tcreated,\n\t);\n\twriteJsonFileIfMissing(AUTH_CONFIG_PATH, {}, \"auth.json\", created);\n\twriteJsonFileIfMissing(MODELS_CONFIG_PATH, MODELS_CONFIG_TEMPLATE, \"models.json\", created);\n\twriteJsonFileIfMissing(SETTINGS_CONFIG_PATH, {}, \"settings.json\", created);\n\n\treturn { created, channelTemplateCreated };\n}\n\nfunction isPlaceholderString(value: string): boolean {\n\treturn value.trim().startsWith(\"your-\");\n}\n\nfunction listChannelConfigIssues(config: Partial<DingTalkAppConfig>): string[] {\n\tconst issues: string[] = [];\n\n\tif (!config.clientId) {\n\t\tissues.push(\"Missing required field `clientId`.\");\n\t} else if (isPlaceholderString(config.clientId)) {\n\t\tissues.push(\"Replace placeholder value for `clientId`.\");\n\t}\n\n\tif (!config.clientSecret) {\n\t\tissues.push(\"Missing required field `clientSecret`.\");\n\t} else if (isPlaceholderString(config.clientSecret)) {\n\t\tissues.push(\"Replace placeholder value for `clientSecret`.\");\n\t}\n\n\tif (config.robotCode && isPlaceholderString(config.robotCode)) {\n\t\tissues.push(\"Replace placeholder value for `robotCode`, or set it to an empty string to reuse `clientId`.\");\n\t}\n\n\tif (config.cardTemplateId && isPlaceholderString(config.cardTemplateId)) {\n\t\tissues.push(\n\t\t\t\"Replace placeholder value for `cardTemplateId`, or set it to an empty string to disable AI Card streaming.\",\n\t\t);\n\t}\n\n\tif (Array.isArray(config.allowFrom) && config.allowFrom.some((value) => isPlaceholderString(value))) {\n\t\tissues.push(\"Replace placeholder values in `allowFrom`, or set it to an empty array to allow all users.\");\n\t}\n\n\treturn issues;\n}\n\nfunction printBootstrapSummary(result: BootstrapResult): void {\n\tif (result.created.length === 0) {\n\t\treturn;\n\t}\n\n\tconsole.log(`Initialized ${APP_NAME} under ${APP_HOME_DIR}:`);\n\tfor (const item of result.created) {\n\t\tconsole.log(` - ${item}`);\n\t}\n\tconsole.log(\"\");\n}\n\nfunction loadConfig(): DingTalkAppConfig {\n\tlet parsed: DingTalkAppConfig;\n\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(CHANNEL_CONFIG_PATH, \"utf-8\")) as DingTalkAppConfig;\n\t} catch (err) {\n\t\tconsole.error(`Failed to parse configuration: ${CHANNEL_CONFIG_PATH}`);\n\t\tconsole.error(err instanceof Error ? err.message : String(err));\n\t\tprocess.exit(1);\n\t}\n\n\tconst issues = listChannelConfigIssues(parsed);\n\tif (issues.length > 0) {\n\t\tconsole.error(`Configuration is not ready: ${CHANNEL_CONFIG_PATH}`);\n\t\tfor (const issue of issues) {\n\t\t\tconsole.error(` - ${issue}`);\n\t\t}\n\t\tconsole.error(\"\");\n\t\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\t\tprocess.exit(1);\n\t}\n\n\tparsed.cardTemplateKey = parsed.cardTemplateKey || \"content\";\n\tparsed.robotCode = parsed.robotCode?.trim() ? parsed.robotCode : parsed.clientId;\n\tif (Array.isArray(parsed.allowFrom)) {\n\t\tparsed.allowFrom = parsed.allowFrom.filter((value) => value.trim().length > 0);\n\t}\n\n\treturn parsed;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\tconsole.log(`Usage: ${APP_NAME} [options]`);\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(\"Options:\");\n\t\t\tconsole.log(\" --sandbox=host Run tools on host (default)\");\n\t\t\tconsole.log(\" --sandbox=docker:<name> Run tools in Docker container\");\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(`Config: ${CHANNEL_CONFIG_PATH}`);\n\t\t\tconsole.log(`Workspace: ${WORKSPACE_DIR}`);\n\t\t\tprocess.exit(0);\n\t\t}\n\t}\n\n\treturn { sandbox };\n}\n\nconst parsedArgs = parseArgs();\nconst sandbox = parsedArgs.sandbox;\nconst bootstrapResult = bootstrapAppHome();\nprintBootstrapSummary(bootstrapResult);\n\nif (bootstrapResult.channelTemplateCreated) {\n\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\tprocess.exit(1);\n}\n\nconst dingtalkConfig = loadConfig();\ndingtalkConfig.stateDir = WORKSPACE_DIR;\n\nawait validateSandbox(sandbox);\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(WORKSPACE_DIR, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir: WORKSPACE_DIR }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\nconst handler: DingTalkHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, _bot: DingTalkBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tlog.logInfo(`[${channelId}] Stop requested`);\n\t\t}\n\t},\n\n\tasync handleEvent(event: DingTalkEvent, bot: DingTalkBot, _isEvent?: boolean): Promise<void> {\n\t\tconst state = getState(event.channelId);\n\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tstate.store.logMessage(event.channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName: event.userName,\n\t\t\ttext: event.text,\n\t\t\tisBot: false,\n\t\t});\n\n\t\ttry {\n\t\t\tconst ctx = createDingTalkContext(event, bot, state.store);\n\t\t\tconst builtInCommand = parseBuiltInCommand(event.text);\n\n\t\t\tif (builtInCommand) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Executing command: ${builtInCommand.rawText}`);\n\t\t\t\tawait state.runner.handleBuiltinCommand(ctx, builtInCommand);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlog.logInfo(`[${event.channelId}] Starting run: ${event.text.substring(0, 50)}`);\n\t\t\tconst result = await state.runner.run(ctx, state.store);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Stopped`);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channelId}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\nlog.logStartup(WORKSPACE_DIR, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nif (!existsSync(WORKSPACE_DIR)) {\n\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n}\n\nconst bot = new DingTalkBot(handler, dingtalkConfig);\nconst eventsWatcher = createEventsWatcher(WORKSPACE_DIR, bot);\neventsWatcher.start();\n\nprocess.on(\"SIGINT\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start();\n"]}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"","sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { parseBuiltInCommand } from \"./commands.js\";\nimport { createDingTalkContext } from \"./delivery.js\";\nimport {\n\ttype BusyMessageMode,\n\tDingTalkBot,\n\ttype DingTalkConfig,\n\ttype DingTalkEvent,\n\ttype DingTalkHandler,\n} from \"./dingtalk.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport {\n\tAPP_HOME_DIR,\n\tAPP_NAME,\n\tAUTH_CONFIG_PATH,\n\tCHANNEL_CONFIG_PATH,\n\tMODELS_CONFIG_PATH,\n\tSETTINGS_CONFIG_PATH,\n\tWORKSPACE_DIR,\n} from \"./paths.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\nif (process.env.DINGTALK_FORCE_PROXY !== \"true\") {\n\tdelete process.env.http_proxy;\n\tdelete process.env.https_proxy;\n\tdelete process.env.all_proxy;\n\tdelete process.env.HTTP_PROXY;\n\tdelete process.env.HTTPS_PROXY;\n\tdelete process.env.ALL_PROXY;\n}\n\ninterface DingTalkAppConfig extends DingTalkConfig {}\n\ninterface ParsedArgs {\n\tsandbox: SandboxConfig;\n}\n\ninterface BootstrapResult {\n\tcreated: string[];\n\tchannelTemplateCreated: boolean;\n}\n\nconst DEFAULT_SOUL = `# SOUL.md\n\nConfigure Pipiclaw's identity, voice, and communication style here.\n\nSuggested sections:\n\n- Who the assistant is\n- Default language\n- Tone and personality\n- Reply style\n- Formatting preferences\n\nExample topics you may want to define:\n\n- \"Answer in Chinese by default.\"\n- \"Be concise and direct.\"\n- \"Prefer Markdown.\"\n- \"Act as an engineering assistant for our team.\"\n\nReplace this template with your actual identity prompt.\n`;\n\nconst DEFAULT_AGENT = `# AGENTS.md\n\nConfigure Pipiclaw's behavioral rules and operating constraints here.\n\nSuggested sections:\n\n- Tool usage policy\n- File access rules\n- Security constraints\n- Approval rules\n- Project-specific workflows\n- Things the assistant must always or never do\n\nExample topics you may want to define:\n\n- Which tools should be preferred first\n- Whether installation commands are allowed\n- How to handle sensitive data\n- Coding conventions for your team\n- Deployment or release restrictions\n\nReplace this template with your actual operating instructions.\n`;\n\nconst DEFAULT_MEMORY = `# 工作记忆\n\n(尚无记录)\n`;\n\nconst CHANNEL_CONFIG_TEMPLATE = {\n\tclientId: \"your-dingtalk-client-id\",\n\tclientSecret: \"your-dingtalk-client-secret\",\n\trobotCode: \"your-robot-code\",\n\tcardTemplateId: \"your-card-template-id\",\n\tcardTemplateKey: \"content\",\n\tallowFrom: [\"your-staff-id\"],\n} satisfies DingTalkAppConfig;\n\nconst MODELS_CONFIG_TEMPLATE = { providers: {} };\n\nfunction writeTextFileIfMissing(path: string, content: string, label: string, created: string[]): boolean {\n\tif (existsSync(path)) {\n\t\treturn false;\n\t}\n\twriteFileSync(path, content, \"utf-8\");\n\tcreated.push(label);\n\treturn true;\n}\n\nfunction writeJsonFileIfMissing(path: string, value: unknown, label: string, created: string[]): boolean {\n\treturn writeTextFileIfMissing(path, `${JSON.stringify(value, null, 2)}\\n`, label, created);\n}\n\nfunction bootstrapAppHome(): BootstrapResult {\n\tconst created: string[] = [];\n\n\tif (!existsSync(APP_HOME_DIR)) {\n\t\tmkdirSync(APP_HOME_DIR, { recursive: true });\n\t\tcreated.push(\"app home\");\n\t}\n\tif (!existsSync(WORKSPACE_DIR)) {\n\t\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n\t\tcreated.push(\"workspace/\");\n\t}\n\n\tfor (const dir of [\"skills\", \"events\"]) {\n\t\tconst dirPath = join(WORKSPACE_DIR, dir);\n\t\tif (!existsSync(dirPath)) {\n\t\t\tmkdirSync(dirPath, { recursive: true });\n\t\t\tcreated.push(`workspace/${dir}/`);\n\t\t}\n\t}\n\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"SOUL.md\"), DEFAULT_SOUL, \"workspace/SOUL.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"AGENTS.md\"), DEFAULT_AGENT, \"workspace/AGENTS.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"MEMORY.md\"), DEFAULT_MEMORY, \"workspace/MEMORY.md\", created);\n\n\tconst channelTemplateCreated = writeJsonFileIfMissing(\n\t\tCHANNEL_CONFIG_PATH,\n\t\tCHANNEL_CONFIG_TEMPLATE,\n\t\t\"channel.json\",\n\t\tcreated,\n\t);\n\twriteJsonFileIfMissing(AUTH_CONFIG_PATH, {}, \"auth.json\", created);\n\twriteJsonFileIfMissing(MODELS_CONFIG_PATH, MODELS_CONFIG_TEMPLATE, \"models.json\", created);\n\twriteJsonFileIfMissing(SETTINGS_CONFIG_PATH, {}, \"settings.json\", created);\n\n\treturn { created, channelTemplateCreated };\n}\n\nfunction isPlaceholderString(value: string): boolean {\n\treturn value.trim().startsWith(\"your-\");\n}\n\nfunction listChannelConfigIssues(config: Partial<DingTalkAppConfig>): string[] {\n\tconst issues: string[] = [];\n\n\tif (!config.clientId) {\n\t\tissues.push(\"Missing required field `clientId`.\");\n\t} else if (isPlaceholderString(config.clientId)) {\n\t\tissues.push(\"Replace placeholder value for `clientId`.\");\n\t}\n\n\tif (!config.clientSecret) {\n\t\tissues.push(\"Missing required field `clientSecret`.\");\n\t} else if (isPlaceholderString(config.clientSecret)) {\n\t\tissues.push(\"Replace placeholder value for `clientSecret`.\");\n\t}\n\n\tif (config.robotCode && isPlaceholderString(config.robotCode)) {\n\t\tissues.push(\"Replace placeholder value for `robotCode`, or set it to an empty string to reuse `clientId`.\");\n\t}\n\n\tif (config.cardTemplateId && isPlaceholderString(config.cardTemplateId)) {\n\t\tissues.push(\n\t\t\t\"Replace placeholder value for `cardTemplateId`, or set it to an empty string to disable AI Card streaming.\",\n\t\t);\n\t}\n\n\tif (Array.isArray(config.allowFrom) && config.allowFrom.some((value) => isPlaceholderString(value))) {\n\t\tissues.push(\"Replace placeholder values in `allowFrom`, or set it to an empty array to allow all users.\");\n\t}\n\n\treturn issues;\n}\n\nfunction printBootstrapSummary(result: BootstrapResult): void {\n\tif (result.created.length === 0) {\n\t\treturn;\n\t}\n\n\tconsole.log(`Initialized ${APP_NAME} under ${APP_HOME_DIR}:`);\n\tfor (const item of result.created) {\n\t\tconsole.log(` - ${item}`);\n\t}\n\tconsole.log(\"\");\n}\n\nfunction loadConfig(): DingTalkAppConfig {\n\tlet parsed: DingTalkAppConfig;\n\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(CHANNEL_CONFIG_PATH, \"utf-8\")) as DingTalkAppConfig;\n\t} catch (err) {\n\t\tconsole.error(`Failed to parse configuration: ${CHANNEL_CONFIG_PATH}`);\n\t\tconsole.error(err instanceof Error ? err.message : String(err));\n\t\tprocess.exit(1);\n\t}\n\n\tconst issues = listChannelConfigIssues(parsed);\n\tif (issues.length > 0) {\n\t\tconsole.error(`Configuration is not ready: ${CHANNEL_CONFIG_PATH}`);\n\t\tfor (const issue of issues) {\n\t\t\tconsole.error(` - ${issue}`);\n\t\t}\n\t\tconsole.error(\"\");\n\t\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\t\tprocess.exit(1);\n\t}\n\n\tparsed.cardTemplateKey = parsed.cardTemplateKey || \"content\";\n\tparsed.robotCode = parsed.robotCode?.trim() ? parsed.robotCode : parsed.clientId;\n\tif (Array.isArray(parsed.allowFrom)) {\n\t\tparsed.allowFrom = parsed.allowFrom.filter((value) => value.trim().length > 0);\n\t}\n\n\treturn parsed;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\tconsole.log(`Usage: ${APP_NAME} [options]`);\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(\"Options:\");\n\t\t\tconsole.log(\" --sandbox=host Run tools on host (default)\");\n\t\t\tconsole.log(\" --sandbox=docker:<name> Run tools in Docker container\");\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(`Config: ${CHANNEL_CONFIG_PATH}`);\n\t\t\tconsole.log(`Workspace: ${WORKSPACE_DIR}`);\n\t\t\tprocess.exit(0);\n\t\t}\n\t}\n\n\treturn { sandbox };\n}\n\nconst parsedArgs = parseArgs();\nconst sandbox = parsedArgs.sandbox;\nconst bootstrapResult = bootstrapAppHome();\nprintBootstrapSummary(bootstrapResult);\n\nif (bootstrapResult.channelTemplateCreated) {\n\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\tprocess.exit(1);\n}\n\nconst dingtalkConfig = loadConfig();\ndingtalkConfig.stateDir = WORKSPACE_DIR;\n\nawait validateSandbox(sandbox);\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(WORKSPACE_DIR, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir: WORKSPACE_DIR }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\nconst handler: DingTalkHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, _bot: DingTalkBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tlog.logInfo(`[${channelId}] Stop requested`);\n\t\t}\n\t},\n\n\tasync handleBusyMessage(\n\t\tevent: DingTalkEvent,\n\t\tbot: DingTalkBot,\n\t\tmode: BusyMessageMode,\n\t\tqueueText: string,\n\t): Promise<void> {\n\t\tconst state = getState(event.channelId);\n\t\tconst trimmedQueueText = queueText.trim();\n\n\t\tawait state.store.logMessage(event.channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName: event.userName,\n\t\t\ttext: event.text,\n\t\t\tisBot: false,\n\t\t\tdeliveryMode: mode,\n\t\t\tskipContextSync: true,\n\t\t});\n\n\t\ttry {\n\t\t\tif (mode === \"followUp\") {\n\t\t\t\tawait state.runner.queueFollowUp(trimmedQueueText);\n\t\t\t} else {\n\t\t\t\tawait state.runner.queueSteer(trimmedQueueText);\n\t\t\t}\n\n\t\t\tconst confirmation =\n\t\t\t\tmode === \"followUp\"\n\t\t\t\t\t? \"Queued as follow-up. I’ll handle it after the current task completes.\"\n\t\t\t\t\t: event.text.trim().startsWith(\"/\")\n\t\t\t\t\t\t? \"Queued as steer. I’ll apply it after the current tool step finishes.\"\n\t\t\t\t\t\t: \"Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.\";\n\t\t\tawait bot.sendPlain(event.channelId, confirmation);\n\t\t\tlog.logInfo(`[${event.channelId}] Queued ${mode}: ${trimmedQueueText.substring(0, 80)}`);\n\t\t} catch (err) {\n\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\tlog.logWarning(`[${event.channelId}] Failed to queue ${mode}`, errMsg);\n\t\t\tawait bot.sendPlain(event.channelId, `Could not queue this message: ${errMsg}`);\n\t\t}\n\t},\n\n\tasync handleEvent(event: DingTalkEvent, bot: DingTalkBot, _isEvent?: boolean): Promise<void> {\n\t\tconst state = getState(event.channelId);\n\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tstate.store.logMessage(event.channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName: event.userName,\n\t\t\ttext: event.text,\n\t\t\tisBot: false,\n\t\t});\n\n\t\ttry {\n\t\t\tconst ctx = createDingTalkContext(event, bot, state.store);\n\t\t\tconst builtInCommand = parseBuiltInCommand(event.text);\n\n\t\t\tif (builtInCommand) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Executing command: ${builtInCommand.rawText}`);\n\t\t\t\tawait state.runner.handleBuiltinCommand(ctx, builtInCommand);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlog.logInfo(`[${event.channelId}] Starting run: ${event.text.substring(0, 50)}`);\n\t\t\tconst result = await state.runner.run(ctx, state.store);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Stopped`);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channelId}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\nlog.logStartup(WORKSPACE_DIR, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nif (!existsSync(WORKSPACE_DIR)) {\n\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n}\n\nconst bot = new DingTalkBot(handler, dingtalkConfig);\nconst eventsWatcher = createEventsWatcher(WORKSPACE_DIR, bot);\neventsWatcher.start();\n\nprocess.on(\"SIGINT\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start();\n"]}
package/dist/main.js CHANGED
@@ -4,7 +4,7 @@ import { join } from "path";
4
4
  import { getOrCreateRunner } from "./agent.js";
5
5
  import { parseBuiltInCommand } from "./commands.js";
6
6
  import { createDingTalkContext } from "./delivery.js";
7
- import { DingTalkBot } from "./dingtalk.js";
7
+ import { DingTalkBot, } from "./dingtalk.js";
8
8
  import { createEventsWatcher } from "./events.js";
9
9
  import * as log from "./log.js";
10
10
  import { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SETTINGS_CONFIG_PATH, WORKSPACE_DIR, } from "./paths.js";
@@ -39,7 +39,7 @@ Example topics you may want to define:
39
39
 
40
40
  Replace this template with your actual identity prompt.
41
41
  `;
42
- const DEFAULT_AGENT = `# AGENT.md
42
+ const DEFAULT_AGENT = `# AGENTS.md
43
43
 
44
44
  Configure Pipiclaw's behavioral rules and operating constraints here.
45
45
 
@@ -104,7 +104,7 @@ function bootstrapAppHome() {
104
104
  }
105
105
  }
106
106
  writeTextFileIfMissing(join(WORKSPACE_DIR, "SOUL.md"), DEFAULT_SOUL, "workspace/SOUL.md", created);
107
- writeTextFileIfMissing(join(WORKSPACE_DIR, "AGENT.md"), DEFAULT_AGENT, "workspace/AGENT.md", created);
107
+ writeTextFileIfMissing(join(WORKSPACE_DIR, "AGENTS.md"), DEFAULT_AGENT, "workspace/AGENTS.md", created);
108
108
  writeTextFileIfMissing(join(WORKSPACE_DIR, "MEMORY.md"), DEFAULT_MEMORY, "workspace/MEMORY.md", created);
109
109
  const channelTemplateCreated = writeJsonFileIfMissing(CHANNEL_CONFIG_PATH, CHANNEL_CONFIG_TEMPLATE, "channel.json", created);
110
110
  writeJsonFileIfMissing(AUTH_CONFIG_PATH, {}, "auth.json", created);
@@ -241,6 +241,40 @@ const handler = {
241
241
  log.logInfo(`[${channelId}] Stop requested`);
242
242
  }
243
243
  },
244
+ async handleBusyMessage(event, bot, mode, queueText) {
245
+ const state = getState(event.channelId);
246
+ const trimmedQueueText = queueText.trim();
247
+ await state.store.logMessage(event.channelId, {
248
+ date: new Date().toISOString(),
249
+ ts: event.ts,
250
+ user: event.user,
251
+ userName: event.userName,
252
+ text: event.text,
253
+ isBot: false,
254
+ deliveryMode: mode,
255
+ skipContextSync: true,
256
+ });
257
+ try {
258
+ if (mode === "followUp") {
259
+ await state.runner.queueFollowUp(trimmedQueueText);
260
+ }
261
+ else {
262
+ await state.runner.queueSteer(trimmedQueueText);
263
+ }
264
+ const confirmation = mode === "followUp"
265
+ ? "Queued as follow-up. I’ll handle it after the current task completes."
266
+ : event.text.trim().startsWith("/")
267
+ ? "Queued as steer. I’ll apply it after the current tool step finishes."
268
+ : "Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.";
269
+ await bot.sendPlain(event.channelId, confirmation);
270
+ log.logInfo(`[${event.channelId}] Queued ${mode}: ${trimmedQueueText.substring(0, 80)}`);
271
+ }
272
+ catch (err) {
273
+ const errMsg = err instanceof Error ? err.message : String(err);
274
+ log.logWarning(`[${event.channelId}] Failed to queue ${mode}`, errMsg);
275
+ await bot.sendPlain(event.channelId, `Could not queue this message: ${errMsg}`);
276
+ }
277
+ },
244
278
  async handleEvent(event, bot, _isEvent) {
245
279
  const state = getState(event.channelId);
246
280
  state.running = true;
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { parseBuiltInCommand } from \"./commands.js\";\nimport { createDingTalkContext } from \"./delivery.js\";\nimport { DingTalkBot, type DingTalkConfig, type DingTalkEvent, type DingTalkHandler } from \"./dingtalk.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport {\n\tAPP_HOME_DIR,\n\tAPP_NAME,\n\tAUTH_CONFIG_PATH,\n\tCHANNEL_CONFIG_PATH,\n\tMODELS_CONFIG_PATH,\n\tSETTINGS_CONFIG_PATH,\n\tWORKSPACE_DIR,\n} from \"./paths.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\nif (process.env.DINGTALK_FORCE_PROXY !== \"true\") {\n\tdelete process.env.http_proxy;\n\tdelete process.env.https_proxy;\n\tdelete process.env.all_proxy;\n\tdelete process.env.HTTP_PROXY;\n\tdelete process.env.HTTPS_PROXY;\n\tdelete process.env.ALL_PROXY;\n}\n\ninterface DingTalkAppConfig extends DingTalkConfig {}\n\ninterface ParsedArgs {\n\tsandbox: SandboxConfig;\n}\n\ninterface BootstrapResult {\n\tcreated: string[];\n\tchannelTemplateCreated: boolean;\n}\n\nconst DEFAULT_SOUL = `# SOUL.md\n\nConfigure Pipiclaw's identity, voice, and communication style here.\n\nSuggested sections:\n\n- Who the assistant is\n- Default language\n- Tone and personality\n- Reply style\n- Formatting preferences\n\nExample topics you may want to define:\n\n- \"Answer in Chinese by default.\"\n- \"Be concise and direct.\"\n- \"Prefer Markdown.\"\n- \"Act as an engineering assistant for our team.\"\n\nReplace this template with your actual identity prompt.\n`;\n\nconst DEFAULT_AGENT = `# AGENT.md\n\nConfigure Pipiclaw's behavioral rules and operating constraints here.\n\nSuggested sections:\n\n- Tool usage policy\n- File access rules\n- Security constraints\n- Approval rules\n- Project-specific workflows\n- Things the assistant must always or never do\n\nExample topics you may want to define:\n\n- Which tools should be preferred first\n- Whether installation commands are allowed\n- How to handle sensitive data\n- Coding conventions for your team\n- Deployment or release restrictions\n\nReplace this template with your actual operating instructions.\n`;\n\nconst DEFAULT_MEMORY = `# 工作记忆\n\n(尚无记录)\n`;\n\nconst CHANNEL_CONFIG_TEMPLATE = {\n\tclientId: \"your-dingtalk-client-id\",\n\tclientSecret: \"your-dingtalk-client-secret\",\n\trobotCode: \"your-robot-code\",\n\tcardTemplateId: \"your-card-template-id\",\n\tcardTemplateKey: \"content\",\n\tallowFrom: [\"your-staff-id\"],\n} satisfies DingTalkAppConfig;\n\nconst MODELS_CONFIG_TEMPLATE = { providers: {} };\n\nfunction writeTextFileIfMissing(path: string, content: string, label: string, created: string[]): boolean {\n\tif (existsSync(path)) {\n\t\treturn false;\n\t}\n\twriteFileSync(path, content, \"utf-8\");\n\tcreated.push(label);\n\treturn true;\n}\n\nfunction writeJsonFileIfMissing(path: string, value: unknown, label: string, created: string[]): boolean {\n\treturn writeTextFileIfMissing(path, `${JSON.stringify(value, null, 2)}\\n`, label, created);\n}\n\nfunction bootstrapAppHome(): BootstrapResult {\n\tconst created: string[] = [];\n\n\tif (!existsSync(APP_HOME_DIR)) {\n\t\tmkdirSync(APP_HOME_DIR, { recursive: true });\n\t\tcreated.push(\"app home\");\n\t}\n\tif (!existsSync(WORKSPACE_DIR)) {\n\t\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n\t\tcreated.push(\"workspace/\");\n\t}\n\n\tfor (const dir of [\"skills\", \"events\"]) {\n\t\tconst dirPath = join(WORKSPACE_DIR, dir);\n\t\tif (!existsSync(dirPath)) {\n\t\t\tmkdirSync(dirPath, { recursive: true });\n\t\t\tcreated.push(`workspace/${dir}/`);\n\t\t}\n\t}\n\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"SOUL.md\"), DEFAULT_SOUL, \"workspace/SOUL.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"AGENT.md\"), DEFAULT_AGENT, \"workspace/AGENT.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"MEMORY.md\"), DEFAULT_MEMORY, \"workspace/MEMORY.md\", created);\n\n\tconst channelTemplateCreated = writeJsonFileIfMissing(\n\t\tCHANNEL_CONFIG_PATH,\n\t\tCHANNEL_CONFIG_TEMPLATE,\n\t\t\"channel.json\",\n\t\tcreated,\n\t);\n\twriteJsonFileIfMissing(AUTH_CONFIG_PATH, {}, \"auth.json\", created);\n\twriteJsonFileIfMissing(MODELS_CONFIG_PATH, MODELS_CONFIG_TEMPLATE, \"models.json\", created);\n\twriteJsonFileIfMissing(SETTINGS_CONFIG_PATH, {}, \"settings.json\", created);\n\n\treturn { created, channelTemplateCreated };\n}\n\nfunction isPlaceholderString(value: string): boolean {\n\treturn value.trim().startsWith(\"your-\");\n}\n\nfunction listChannelConfigIssues(config: Partial<DingTalkAppConfig>): string[] {\n\tconst issues: string[] = [];\n\n\tif (!config.clientId) {\n\t\tissues.push(\"Missing required field `clientId`.\");\n\t} else if (isPlaceholderString(config.clientId)) {\n\t\tissues.push(\"Replace placeholder value for `clientId`.\");\n\t}\n\n\tif (!config.clientSecret) {\n\t\tissues.push(\"Missing required field `clientSecret`.\");\n\t} else if (isPlaceholderString(config.clientSecret)) {\n\t\tissues.push(\"Replace placeholder value for `clientSecret`.\");\n\t}\n\n\tif (config.robotCode && isPlaceholderString(config.robotCode)) {\n\t\tissues.push(\"Replace placeholder value for `robotCode`, or set it to an empty string to reuse `clientId`.\");\n\t}\n\n\tif (config.cardTemplateId && isPlaceholderString(config.cardTemplateId)) {\n\t\tissues.push(\n\t\t\t\"Replace placeholder value for `cardTemplateId`, or set it to an empty string to disable AI Card streaming.\",\n\t\t);\n\t}\n\n\tif (Array.isArray(config.allowFrom) && config.allowFrom.some((value) => isPlaceholderString(value))) {\n\t\tissues.push(\"Replace placeholder values in `allowFrom`, or set it to an empty array to allow all users.\");\n\t}\n\n\treturn issues;\n}\n\nfunction printBootstrapSummary(result: BootstrapResult): void {\n\tif (result.created.length === 0) {\n\t\treturn;\n\t}\n\n\tconsole.log(`Initialized ${APP_NAME} under ${APP_HOME_DIR}:`);\n\tfor (const item of result.created) {\n\t\tconsole.log(` - ${item}`);\n\t}\n\tconsole.log(\"\");\n}\n\nfunction loadConfig(): DingTalkAppConfig {\n\tlet parsed: DingTalkAppConfig;\n\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(CHANNEL_CONFIG_PATH, \"utf-8\")) as DingTalkAppConfig;\n\t} catch (err) {\n\t\tconsole.error(`Failed to parse configuration: ${CHANNEL_CONFIG_PATH}`);\n\t\tconsole.error(err instanceof Error ? err.message : String(err));\n\t\tprocess.exit(1);\n\t}\n\n\tconst issues = listChannelConfigIssues(parsed);\n\tif (issues.length > 0) {\n\t\tconsole.error(`Configuration is not ready: ${CHANNEL_CONFIG_PATH}`);\n\t\tfor (const issue of issues) {\n\t\t\tconsole.error(` - ${issue}`);\n\t\t}\n\t\tconsole.error(\"\");\n\t\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\t\tprocess.exit(1);\n\t}\n\n\tparsed.cardTemplateKey = parsed.cardTemplateKey || \"content\";\n\tparsed.robotCode = parsed.robotCode?.trim() ? parsed.robotCode : parsed.clientId;\n\tif (Array.isArray(parsed.allowFrom)) {\n\t\tparsed.allowFrom = parsed.allowFrom.filter((value) => value.trim().length > 0);\n\t}\n\n\treturn parsed;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\tconsole.log(`Usage: ${APP_NAME} [options]`);\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(\"Options:\");\n\t\t\tconsole.log(\" --sandbox=host Run tools on host (default)\");\n\t\t\tconsole.log(\" --sandbox=docker:<name> Run tools in Docker container\");\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(`Config: ${CHANNEL_CONFIG_PATH}`);\n\t\t\tconsole.log(`Workspace: ${WORKSPACE_DIR}`);\n\t\t\tprocess.exit(0);\n\t\t}\n\t}\n\n\treturn { sandbox };\n}\n\nconst parsedArgs = parseArgs();\nconst sandbox = parsedArgs.sandbox;\nconst bootstrapResult = bootstrapAppHome();\nprintBootstrapSummary(bootstrapResult);\n\nif (bootstrapResult.channelTemplateCreated) {\n\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\tprocess.exit(1);\n}\n\nconst dingtalkConfig = loadConfig();\ndingtalkConfig.stateDir = WORKSPACE_DIR;\n\nawait validateSandbox(sandbox);\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(WORKSPACE_DIR, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir: WORKSPACE_DIR }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\nconst handler: DingTalkHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, _bot: DingTalkBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tlog.logInfo(`[${channelId}] Stop requested`);\n\t\t}\n\t},\n\n\tasync handleEvent(event: DingTalkEvent, bot: DingTalkBot, _isEvent?: boolean): Promise<void> {\n\t\tconst state = getState(event.channelId);\n\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tstate.store.logMessage(event.channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName: event.userName,\n\t\t\ttext: event.text,\n\t\t\tisBot: false,\n\t\t});\n\n\t\ttry {\n\t\t\tconst ctx = createDingTalkContext(event, bot, state.store);\n\t\t\tconst builtInCommand = parseBuiltInCommand(event.text);\n\n\t\t\tif (builtInCommand) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Executing command: ${builtInCommand.rawText}`);\n\t\t\t\tawait state.runner.handleBuiltinCommand(ctx, builtInCommand);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlog.logInfo(`[${event.channelId}] Starting run: ${event.text.substring(0, 50)}`);\n\t\t\tconst result = await state.runner.run(ctx, state.store);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Stopped`);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channelId}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\nlog.logStartup(WORKSPACE_DIR, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nif (!existsSync(WORKSPACE_DIR)) {\n\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n}\n\nconst bot = new DingTalkBot(handler, dingtalkConfig);\nconst eventsWatcher = createEventsWatcher(WORKSPACE_DIR, bot);\neventsWatcher.start();\n\nprocess.on(\"SIGINT\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start();\n"]}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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","sourcesContent":["#!/usr/bin/env node\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { type AgentRunner, getOrCreateRunner } from \"./agent.js\";\nimport { parseBuiltInCommand } from \"./commands.js\";\nimport { createDingTalkContext } from \"./delivery.js\";\nimport {\n\ttype BusyMessageMode,\n\tDingTalkBot,\n\ttype DingTalkConfig,\n\ttype DingTalkEvent,\n\ttype DingTalkHandler,\n} from \"./dingtalk.js\";\nimport { createEventsWatcher } from \"./events.js\";\nimport * as log from \"./log.js\";\nimport {\n\tAPP_HOME_DIR,\n\tAPP_NAME,\n\tAUTH_CONFIG_PATH,\n\tCHANNEL_CONFIG_PATH,\n\tMODELS_CONFIG_PATH,\n\tSETTINGS_CONFIG_PATH,\n\tWORKSPACE_DIR,\n} from \"./paths.js\";\nimport { parseSandboxArg, type SandboxConfig, validateSandbox } from \"./sandbox.js\";\nimport { ChannelStore } from \"./store.js\";\n\nif (process.env.DINGTALK_FORCE_PROXY !== \"true\") {\n\tdelete process.env.http_proxy;\n\tdelete process.env.https_proxy;\n\tdelete process.env.all_proxy;\n\tdelete process.env.HTTP_PROXY;\n\tdelete process.env.HTTPS_PROXY;\n\tdelete process.env.ALL_PROXY;\n}\n\ninterface DingTalkAppConfig extends DingTalkConfig {}\n\ninterface ParsedArgs {\n\tsandbox: SandboxConfig;\n}\n\ninterface BootstrapResult {\n\tcreated: string[];\n\tchannelTemplateCreated: boolean;\n}\n\nconst DEFAULT_SOUL = `# SOUL.md\n\nConfigure Pipiclaw's identity, voice, and communication style here.\n\nSuggested sections:\n\n- Who the assistant is\n- Default language\n- Tone and personality\n- Reply style\n- Formatting preferences\n\nExample topics you may want to define:\n\n- \"Answer in Chinese by default.\"\n- \"Be concise and direct.\"\n- \"Prefer Markdown.\"\n- \"Act as an engineering assistant for our team.\"\n\nReplace this template with your actual identity prompt.\n`;\n\nconst DEFAULT_AGENT = `# AGENTS.md\n\nConfigure Pipiclaw's behavioral rules and operating constraints here.\n\nSuggested sections:\n\n- Tool usage policy\n- File access rules\n- Security constraints\n- Approval rules\n- Project-specific workflows\n- Things the assistant must always or never do\n\nExample topics you may want to define:\n\n- Which tools should be preferred first\n- Whether installation commands are allowed\n- How to handle sensitive data\n- Coding conventions for your team\n- Deployment or release restrictions\n\nReplace this template with your actual operating instructions.\n`;\n\nconst DEFAULT_MEMORY = `# 工作记忆\n\n(尚无记录)\n`;\n\nconst CHANNEL_CONFIG_TEMPLATE = {\n\tclientId: \"your-dingtalk-client-id\",\n\tclientSecret: \"your-dingtalk-client-secret\",\n\trobotCode: \"your-robot-code\",\n\tcardTemplateId: \"your-card-template-id\",\n\tcardTemplateKey: \"content\",\n\tallowFrom: [\"your-staff-id\"],\n} satisfies DingTalkAppConfig;\n\nconst MODELS_CONFIG_TEMPLATE = { providers: {} };\n\nfunction writeTextFileIfMissing(path: string, content: string, label: string, created: string[]): boolean {\n\tif (existsSync(path)) {\n\t\treturn false;\n\t}\n\twriteFileSync(path, content, \"utf-8\");\n\tcreated.push(label);\n\treturn true;\n}\n\nfunction writeJsonFileIfMissing(path: string, value: unknown, label: string, created: string[]): boolean {\n\treturn writeTextFileIfMissing(path, `${JSON.stringify(value, null, 2)}\\n`, label, created);\n}\n\nfunction bootstrapAppHome(): BootstrapResult {\n\tconst created: string[] = [];\n\n\tif (!existsSync(APP_HOME_DIR)) {\n\t\tmkdirSync(APP_HOME_DIR, { recursive: true });\n\t\tcreated.push(\"app home\");\n\t}\n\tif (!existsSync(WORKSPACE_DIR)) {\n\t\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n\t\tcreated.push(\"workspace/\");\n\t}\n\n\tfor (const dir of [\"skills\", \"events\"]) {\n\t\tconst dirPath = join(WORKSPACE_DIR, dir);\n\t\tif (!existsSync(dirPath)) {\n\t\t\tmkdirSync(dirPath, { recursive: true });\n\t\t\tcreated.push(`workspace/${dir}/`);\n\t\t}\n\t}\n\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"SOUL.md\"), DEFAULT_SOUL, \"workspace/SOUL.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"AGENTS.md\"), DEFAULT_AGENT, \"workspace/AGENTS.md\", created);\n\twriteTextFileIfMissing(join(WORKSPACE_DIR, \"MEMORY.md\"), DEFAULT_MEMORY, \"workspace/MEMORY.md\", created);\n\n\tconst channelTemplateCreated = writeJsonFileIfMissing(\n\t\tCHANNEL_CONFIG_PATH,\n\t\tCHANNEL_CONFIG_TEMPLATE,\n\t\t\"channel.json\",\n\t\tcreated,\n\t);\n\twriteJsonFileIfMissing(AUTH_CONFIG_PATH, {}, \"auth.json\", created);\n\twriteJsonFileIfMissing(MODELS_CONFIG_PATH, MODELS_CONFIG_TEMPLATE, \"models.json\", created);\n\twriteJsonFileIfMissing(SETTINGS_CONFIG_PATH, {}, \"settings.json\", created);\n\n\treturn { created, channelTemplateCreated };\n}\n\nfunction isPlaceholderString(value: string): boolean {\n\treturn value.trim().startsWith(\"your-\");\n}\n\nfunction listChannelConfigIssues(config: Partial<DingTalkAppConfig>): string[] {\n\tconst issues: string[] = [];\n\n\tif (!config.clientId) {\n\t\tissues.push(\"Missing required field `clientId`.\");\n\t} else if (isPlaceholderString(config.clientId)) {\n\t\tissues.push(\"Replace placeholder value for `clientId`.\");\n\t}\n\n\tif (!config.clientSecret) {\n\t\tissues.push(\"Missing required field `clientSecret`.\");\n\t} else if (isPlaceholderString(config.clientSecret)) {\n\t\tissues.push(\"Replace placeholder value for `clientSecret`.\");\n\t}\n\n\tif (config.robotCode && isPlaceholderString(config.robotCode)) {\n\t\tissues.push(\"Replace placeholder value for `robotCode`, or set it to an empty string to reuse `clientId`.\");\n\t}\n\n\tif (config.cardTemplateId && isPlaceholderString(config.cardTemplateId)) {\n\t\tissues.push(\n\t\t\t\"Replace placeholder value for `cardTemplateId`, or set it to an empty string to disable AI Card streaming.\",\n\t\t);\n\t}\n\n\tif (Array.isArray(config.allowFrom) && config.allowFrom.some((value) => isPlaceholderString(value))) {\n\t\tissues.push(\"Replace placeholder values in `allowFrom`, or set it to an empty array to allow all users.\");\n\t}\n\n\treturn issues;\n}\n\nfunction printBootstrapSummary(result: BootstrapResult): void {\n\tif (result.created.length === 0) {\n\t\treturn;\n\t}\n\n\tconsole.log(`Initialized ${APP_NAME} under ${APP_HOME_DIR}:`);\n\tfor (const item of result.created) {\n\t\tconsole.log(` - ${item}`);\n\t}\n\tconsole.log(\"\");\n}\n\nfunction loadConfig(): DingTalkAppConfig {\n\tlet parsed: DingTalkAppConfig;\n\n\ttry {\n\t\tparsed = JSON.parse(readFileSync(CHANNEL_CONFIG_PATH, \"utf-8\")) as DingTalkAppConfig;\n\t} catch (err) {\n\t\tconsole.error(`Failed to parse configuration: ${CHANNEL_CONFIG_PATH}`);\n\t\tconsole.error(err instanceof Error ? err.message : String(err));\n\t\tprocess.exit(1);\n\t}\n\n\tconst issues = listChannelConfigIssues(parsed);\n\tif (issues.length > 0) {\n\t\tconsole.error(`Configuration is not ready: ${CHANNEL_CONFIG_PATH}`);\n\t\tfor (const issue of issues) {\n\t\t\tconsole.error(` - ${issue}`);\n\t\t}\n\t\tconsole.error(\"\");\n\t\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\t\tprocess.exit(1);\n\t}\n\n\tparsed.cardTemplateKey = parsed.cardTemplateKey || \"content\";\n\tparsed.robotCode = parsed.robotCode?.trim() ? parsed.robotCode : parsed.clientId;\n\tif (Array.isArray(parsed.allowFrom)) {\n\t\tparsed.allowFrom = parsed.allowFrom.filter((value) => value.trim().length > 0);\n\t}\n\n\treturn parsed;\n}\n\nfunction parseArgs(): ParsedArgs {\n\tconst args = process.argv.slice(2);\n\tlet sandbox: SandboxConfig = { type: \"host\" };\n\n\tfor (let i = 0; i < args.length; i++) {\n\t\tconst arg = args[i];\n\t\tif (arg.startsWith(\"--sandbox=\")) {\n\t\t\tsandbox = parseSandboxArg(arg.slice(\"--sandbox=\".length));\n\t\t} else if (arg === \"--sandbox\") {\n\t\t\tsandbox = parseSandboxArg(args[++i] || \"\");\n\t\t} else if (arg === \"--help\" || arg === \"-h\") {\n\t\t\tconsole.log(`Usage: ${APP_NAME} [options]`);\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(\"Options:\");\n\t\t\tconsole.log(\" --sandbox=host Run tools on host (default)\");\n\t\t\tconsole.log(\" --sandbox=docker:<name> Run tools in Docker container\");\n\t\t\tconsole.log(\"\");\n\t\t\tconsole.log(`Config: ${CHANNEL_CONFIG_PATH}`);\n\t\t\tconsole.log(`Workspace: ${WORKSPACE_DIR}`);\n\t\t\tprocess.exit(0);\n\t\t}\n\t}\n\n\treturn { sandbox };\n}\n\nconst parsedArgs = parseArgs();\nconst sandbox = parsedArgs.sandbox;\nconst bootstrapResult = bootstrapAppHome();\nprintBootstrapSummary(bootstrapResult);\n\nif (bootstrapResult.channelTemplateCreated) {\n\tconsole.error(`Fill in ${CHANNEL_CONFIG_PATH} and run \\`${APP_NAME}\\` again.`);\n\tprocess.exit(1);\n}\n\nconst dingtalkConfig = loadConfig();\ndingtalkConfig.stateDir = WORKSPACE_DIR;\n\nawait validateSandbox(sandbox);\n\ninterface ChannelState {\n\trunning: boolean;\n\trunner: AgentRunner;\n\tstore: ChannelStore;\n\tstopRequested: boolean;\n}\n\nconst channelStates = new Map<string, ChannelState>();\n\nfunction getState(channelId: string): ChannelState {\n\tlet state = channelStates.get(channelId);\n\tif (!state) {\n\t\tconst channelDir = join(WORKSPACE_DIR, channelId);\n\t\tstate = {\n\t\t\trunning: false,\n\t\t\trunner: getOrCreateRunner(sandbox, channelId, channelDir),\n\t\t\tstore: new ChannelStore({ workingDir: WORKSPACE_DIR }),\n\t\t\tstopRequested: false,\n\t\t};\n\t\tchannelStates.set(channelId, state);\n\t}\n\treturn state;\n}\n\nconst handler: DingTalkHandler = {\n\tisRunning(channelId: string): boolean {\n\t\tconst state = channelStates.get(channelId);\n\t\treturn state?.running ?? false;\n\t},\n\n\tasync handleStop(channelId: string, _bot: DingTalkBot): Promise<void> {\n\t\tconst state = channelStates.get(channelId);\n\t\tif (state?.running) {\n\t\t\tstate.stopRequested = true;\n\t\t\tstate.runner.abort();\n\t\t\tlog.logInfo(`[${channelId}] Stop requested`);\n\t\t}\n\t},\n\n\tasync handleBusyMessage(\n\t\tevent: DingTalkEvent,\n\t\tbot: DingTalkBot,\n\t\tmode: BusyMessageMode,\n\t\tqueueText: string,\n\t): Promise<void> {\n\t\tconst state = getState(event.channelId);\n\t\tconst trimmedQueueText = queueText.trim();\n\n\t\tawait state.store.logMessage(event.channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName: event.userName,\n\t\t\ttext: event.text,\n\t\t\tisBot: false,\n\t\t\tdeliveryMode: mode,\n\t\t\tskipContextSync: true,\n\t\t});\n\n\t\ttry {\n\t\t\tif (mode === \"followUp\") {\n\t\t\t\tawait state.runner.queueFollowUp(trimmedQueueText);\n\t\t\t} else {\n\t\t\t\tawait state.runner.queueSteer(trimmedQueueText);\n\t\t\t}\n\n\t\t\tconst confirmation =\n\t\t\t\tmode === \"followUp\"\n\t\t\t\t\t? \"Queued as follow-up. I’ll handle it after the current task completes.\"\n\t\t\t\t\t: event.text.trim().startsWith(\"/\")\n\t\t\t\t\t\t? \"Queued as steer. I’ll apply it after the current tool step finishes.\"\n\t\t\t\t\t\t: \"Queued as steer. I’ll apply this after the current tool step finishes. Use `/followup <message>` to queue it after completion.\";\n\t\t\tawait bot.sendPlain(event.channelId, confirmation);\n\t\t\tlog.logInfo(`[${event.channelId}] Queued ${mode}: ${trimmedQueueText.substring(0, 80)}`);\n\t\t} catch (err) {\n\t\t\tconst errMsg = err instanceof Error ? err.message : String(err);\n\t\t\tlog.logWarning(`[${event.channelId}] Failed to queue ${mode}`, errMsg);\n\t\t\tawait bot.sendPlain(event.channelId, `Could not queue this message: ${errMsg}`);\n\t\t}\n\t},\n\n\tasync handleEvent(event: DingTalkEvent, bot: DingTalkBot, _isEvent?: boolean): Promise<void> {\n\t\tconst state = getState(event.channelId);\n\n\t\tstate.running = true;\n\t\tstate.stopRequested = false;\n\n\t\tstate.store.logMessage(event.channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts: event.ts,\n\t\t\tuser: event.user,\n\t\t\tuserName: event.userName,\n\t\t\ttext: event.text,\n\t\t\tisBot: false,\n\t\t});\n\n\t\ttry {\n\t\t\tconst ctx = createDingTalkContext(event, bot, state.store);\n\t\t\tconst builtInCommand = parseBuiltInCommand(event.text);\n\n\t\t\tif (builtInCommand) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Executing command: ${builtInCommand.rawText}`);\n\t\t\t\tawait state.runner.handleBuiltinCommand(ctx, builtInCommand);\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tlog.logInfo(`[${event.channelId}] Starting run: ${event.text.substring(0, 50)}`);\n\t\t\tconst result = await state.runner.run(ctx, state.store);\n\n\t\t\tif (result.stopReason === \"aborted\" && state.stopRequested) {\n\t\t\t\tlog.logInfo(`[${event.channelId}] Stopped`);\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tlog.logWarning(`[${event.channelId}] Run error`, err instanceof Error ? err.message : String(err));\n\t\t} finally {\n\t\t\tstate.running = false;\n\t\t}\n\t},\n};\n\nlog.logStartup(WORKSPACE_DIR, sandbox.type === \"host\" ? \"host\" : `docker:${sandbox.container}`);\n\nif (!existsSync(WORKSPACE_DIR)) {\n\tmkdirSync(WORKSPACE_DIR, { recursive: true });\n}\n\nconst bot = new DingTalkBot(handler, dingtalkConfig);\nconst eventsWatcher = createEventsWatcher(WORKSPACE_DIR, bot);\neventsWatcher.start();\n\nprocess.on(\"SIGINT\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nprocess.on(\"SIGTERM\", () => {\n\tlog.logInfo(\"Shutting down...\");\n\teventsWatcher.stop();\n\tprocess.exit(0);\n});\n\nbot.start();\n"]}
package/dist/store.d.ts CHANGED
@@ -6,6 +6,8 @@ export interface LoggedMessage {
6
6
  displayName?: string;
7
7
  text: string;
8
8
  isBot: boolean;
9
+ deliveryMode?: "steer" | "followUp";
10
+ skipContextSync?: boolean;
9
11
  }
10
12
  export interface ChannelStoreConfig {
11
13
  workingDir: string;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAOrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAwB5E;IAED;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBjD;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAOrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;;OAGG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAwB5E;IAED;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkBjD;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAC/E,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAgBrC,MAAM,OAAO,YAAY;IAChB,UAAU,CAAS;IAC3B,iEAAiE;IACzD,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;IAEnD,YAAY,MAA0B,EAAE;QACvC,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QAEpC,kCAAkC;QAClC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;YAClC,SAAS,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;IAAA,CACD;IAED;;OAEG;IACH,aAAa,CAAC,SAAiB,EAAU;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACtB,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACrC,CAAC;QACD,OAAO,GAAG,CAAC;IAAA,CACX;IAED;;;OAGG;IACH,KAAK,CAAC,UAAU,CAAC,SAAiB,EAAE,OAAsB,EAAoB;QAC7E,iDAAiD;QACjD,MAAM,SAAS,GAAG,GAAG,SAAS,IAAI,OAAO,CAAC,EAAE,EAAE,CAAC;QAC/C,IAAI,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YACxC,OAAO,KAAK,CAAC,CAAC,iBAAiB;QAChC,CAAC;QAED,uDAAuD;QACvD,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC/C,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,KAAK,CAAC,CAAC;QAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC,CAAC;QAEjE,oCAAoC;QACpC,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAE7B,kCAAkC;QAClC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACnB,OAAO,CAAC,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACzC,CAAC;QAED,MAAM,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC;QAC5C,MAAM,UAAU,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;QACzC,OAAO,IAAI,CAAC;IAAA,CACZ;IAED;;;OAGG;IACK,cAAc,CAAC,OAAe,EAAQ;QAC7C,IAAI,CAAC;YACJ,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC;gBAAE,OAAO;YACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;YAChC,IAAI,KAAK,CAAC,IAAI,GAAG,SAAS,EAAE,CAAC;gBAC5B,UAAU,CAAC,OAAO,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC;gBACpC,iDAAiD;gBACjD,MAAM,cAAc,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC,CAAC;gBAC9D,IAAI,CAAC;oBACJ,SAAS,CAAC,cAAc,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAC,CAAC,CAAC,CAAC;gBACzD,CAAC;gBAAC,MAAM,CAAC;oBACR,YAAY;gBACb,CAAC;YACF,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,yBAAyB;QAC1B,CAAC;IAAA,CACD;IAED;;OAEG;IACH,KAAK,CAAC,cAAc,CAAC,SAAiB,EAAE,IAAY,EAAE,EAAU,EAAiB;QAChF,MAAM,IAAI,CAAC,UAAU,CAAC,SAAS,EAAE;YAChC,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YAC9B,EAAE;YACF,IAAI,EAAE,KAAK;YACX,IAAI;YACJ,KAAK,EAAE,IAAI;SACX,CAAC,CAAC;IAAA,CACH;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAiB,EAAiB;QAClD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1B,OAAO,IAAI,CAAC;QACb,CAAC;QAED,IAAI,CAAC;YACJ,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC/C,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACzC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC;gBAC3C,OAAO,IAAI,CAAC;YACb,CAAC;YACD,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAkB,CAAC;YACtD,OAAO,OAAO,CAAC,EAAE,CAAC;QACnB,CAAC;QAAC,MAAM,CAAC;YACR,OAAO,IAAI,CAAC;QACb,CAAC;IAAA,CACD;CACD","sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { existsSync, mkdirSync, readFileSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst content = readFileSync(logPath, \"utf-8\");\n\t\t\tconst lines = content.trim().split(\"\\n\");\n\t\t\tif (lines.length === 0 || lines[0] === \"\") {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tconst lastLine = lines[lines.length - 1];\n\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\treturn message.ts;\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Pipiclaw DingTalk bot powered by the pi coding agent",
5
5
  "type": "module",
6
6
  "bin": {