@iinm/plain-agent 1.5.4 → 1.6.1

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,128 @@
1
+ import { Transform } from "node:stream";
2
+
3
+ // Bracketed paste mode sequences
4
+ const BRACKETED_PASTE_START = "\x1b[200~";
5
+ const BRACKETED_PASTE_END = "\x1b[201~";
6
+
7
+ // Store for pasted content
8
+ const pastedContentStore = new Map();
9
+
10
+ /**
11
+ * Generate a short hash for paste reference
12
+ * @param {string} content
13
+ * @returns {string}
14
+ */
15
+ function generatePasteHash(content) {
16
+ let hash = 0;
17
+ for (let i = 0; i < content.length; i++) {
18
+ const char = content.charCodeAt(i);
19
+ hash = (hash << 5) - hash + char;
20
+ hash = hash & hash; // Convert to 32bit integer
21
+ }
22
+ return Math.abs(hash).toString(16).padStart(6, "0").slice(0, 6);
23
+ }
24
+
25
+ /**
26
+ * Resolve paste placeholders and append context tags
27
+ * @param {string} input
28
+ * @returns {string}
29
+ */
30
+ export function resolvePastePlaceholders(input) {
31
+ /** @type {string[]} */
32
+ const contexts = [];
33
+
34
+ // Collect paste content for context tags while keeping placeholders
35
+ const text = input.replace(/\[pasted#([a-f0-9]{6})\]/g, (match, hash) => {
36
+ const content = pastedContentStore.get(hash);
37
+ if (content !== undefined) {
38
+ pastedContentStore.delete(hash); // Clean up after use
39
+ contexts.push(
40
+ `<context location="pasted#${hash}">\n${content}\n</context>`,
41
+ );
42
+ }
43
+ return match; // Keep placeholder in text
44
+ });
45
+
46
+ // Append contexts to the end of input
47
+ if (contexts.length > 0) {
48
+ return [text, ...contexts].join("\n\n");
49
+ }
50
+
51
+ return text;
52
+ }
53
+
54
+ /**
55
+ * Create a Transform stream to handle bracketed paste before readline.
56
+ * @param {() => void} onExitRequest - Called when Ctrl-C or Ctrl-D is detected
57
+ * @returns {Transform}
58
+ */
59
+ export function createPasteTransform(onExitRequest) {
60
+ let inPasteMode = false;
61
+ let pasteBuffer = "";
62
+
63
+ return new Transform({
64
+ transform(chunk, _encoding, callback) {
65
+ /** @type {string} */
66
+ let data = chunk.toString("utf8");
67
+
68
+ // Handle Ctrl-C and Ctrl-D
69
+ if (data.includes("\x03") || data.includes("\x04")) {
70
+ // Ctrl-C / Ctrl-D: request exit (handled by confirmExit)
71
+ onExitRequest();
72
+ callback();
73
+ return;
74
+ }
75
+
76
+ while (data.length > 0) {
77
+ if (inPasteMode) {
78
+ const endIdx = data.indexOf(BRACKETED_PASTE_END);
79
+ if (endIdx !== -1) {
80
+ // End of paste
81
+ pasteBuffer += data.slice(0, endIdx);
82
+ data = data.slice(endIdx + BRACKETED_PASTE_END.length);
83
+ inPasteMode = false;
84
+
85
+ // Handle paste content
86
+ if (pasteBuffer) {
87
+ // Remove trailing newline for single-line paste detection
88
+ const trimmedPaste = pasteBuffer.replace(/\n$/, "");
89
+
90
+ // For single-line paste, insert directly without placeholder
91
+ if (!trimmedPaste.includes("\n")) {
92
+ this.push(trimmedPaste);
93
+ } else {
94
+ // For multi-line paste, use placeholder
95
+ const hash = generatePasteHash(pasteBuffer);
96
+ pastedContentStore.set(hash, pasteBuffer);
97
+ this.push(`[pasted#${hash}] `);
98
+ }
99
+ }
100
+ pasteBuffer = "";
101
+ } else {
102
+ // Still in paste mode
103
+ pasteBuffer += data;
104
+ break;
105
+ }
106
+ } else {
107
+ const startIdx = data.indexOf(BRACKETED_PASTE_START);
108
+ if (startIdx !== -1) {
109
+ // Start of paste
110
+ // Output any data before the paste
111
+ if (startIdx > 0) {
112
+ this.push(data.slice(0, startIdx));
113
+ }
114
+ data = data.slice(startIdx + BRACKETED_PASTE_START.length);
115
+ inPasteMode = true;
116
+ pasteBuffer = "";
117
+ } else {
118
+ // Normal data
119
+ this.push(data);
120
+ break;
121
+ }
122
+ }
123
+ }
124
+
125
+ callback();
126
+ },
127
+ });
128
+ }
@@ -17,6 +17,7 @@ import {
17
17
  * @property {string} description
18
18
  * @property {string} content
19
19
  * @property {string} filePath
20
+ * @property {boolean} claudeOriginated
20
21
  * @property {string} [import]
21
22
  */
22
23
 
@@ -224,6 +225,7 @@ async function getMarkdownFiles(dir, baseDir = dir) {
224
225
  function parseAgentRole(relativePath, fileContent, fullPath, idPrefix = "") {
225
226
  const rawId = relativePath.replace(/\.md$/, "");
226
227
  const id = idPrefix + rawId;
228
+ const claudeOriginated = idPrefix.startsWith("claude");
227
229
 
228
230
  // Match YAML frontmatter
229
231
  const match = fileContent.match(
@@ -236,6 +238,7 @@ function parseAgentRole(relativePath, fileContent, fullPath, idPrefix = "") {
236
238
  description: "",
237
239
  content: fileContent.trim(),
238
240
  filePath: fullPath,
241
+ claudeOriginated,
239
242
  };
240
243
  }
241
244
 
@@ -251,6 +254,7 @@ function parseAgentRole(relativePath, fileContent, fullPath, idPrefix = "") {
251
254
  description: parseFrontmatterField(match[1], "description") ?? "",
252
255
  content: fileContent.trim(),
253
256
  filePath: fullPath,
257
+ claudeOriginated,
254
258
  };
255
259
  }
256
260
  const content = match[2].trim();
@@ -260,6 +264,7 @@ function parseAgentRole(relativePath, fileContent, fullPath, idPrefix = "") {
260
264
  description: frontmatter.description ?? "",
261
265
  content,
262
266
  filePath: fullPath,
267
+ claudeOriginated,
263
268
  import: frontmatter.import,
264
269
  };
265
270
  }
@@ -18,6 +18,7 @@ import {
18
18
  * @property {string} description
19
19
  * @property {string} content
20
20
  * @property {string} filePath
21
+ * @property {boolean} claudeOriginated
21
22
  * @property {string} [import]
22
23
  * @property {boolean} [userInvocable]
23
24
  * @property {boolean} [isShortcut]
@@ -252,6 +253,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
252
253
  const id = isShortcut
253
254
  ? idPrefix + rawId.replace(/^shortcuts\//, "")
254
255
  : idPrefix + rawId;
256
+ const claudeOriginated = idPrefix.startsWith("claude");
255
257
 
256
258
  // Match YAML frontmatter
257
259
  const match = fileContent.match(
@@ -264,6 +266,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
264
266
  description: "",
265
267
  content: fileContent.trim(),
266
268
  filePath: fullPath,
269
+ claudeOriginated,
267
270
  isShortcut,
268
271
  isSkill,
269
272
  };
@@ -284,6 +287,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
284
287
  description: parseFrontmatterField(match[1], "description") ?? "",
285
288
  content,
286
289
  filePath: fullPath,
290
+ claudeOriginated,
287
291
  import: parseFrontmatterField(match[1], "import"),
288
292
  userInvocable:
289
293
  parseFrontmatterField(match[1], "user-invocable") === "true" ||
@@ -299,6 +303,7 @@ function parsePrompt(relativePath, fileContent, fullPath, idPrefix = "") {
299
303
  description: frontmatter.description ?? "",
300
304
  content,
301
305
  filePath: fullPath,
306
+ claudeOriginated,
302
307
  import: frontmatter.import,
303
308
  userInvocable: userInvocable ?? undefined,
304
309
  isShortcut,
package/src/prompt.mjs CHANGED
@@ -112,12 +112,6 @@ If skill matches task: read full file and apply the workflow
112
112
 
113
113
  ${skillDescriptions}
114
114
 
115
- # Claude Code Compatibility Notes
116
-
117
- When using a Claude Code-compatible command, agent, or skill, follow these rules:
118
- - Subagents cannot run in parallel. Delegate to them one at a time.
119
- - If a Claude Code prompt mentions CLAUDE.md for project rules or conventions, use AGENTS.md instead when CLAUDE.md is absent.
120
-
121
115
  # Environment
122
116
 
123
117
  - User name: ${username}
@@ -132,3 +126,10 @@ ${agentRoleDescriptions}
132
126
  - custom:<role-name>: Use this for ad-hoc roles not listed above (e.g., custom:explore, custom:plan).
133
127
  `.trim();
134
128
  }
129
+
130
+ export const CLAUDE_CODE_COMPATIBILITY_NOTES = `# Environment Constraints
131
+
132
+ - Use memory file to manage todo list.
133
+ - Subagents cannot run in parallel. Delegate to them one at a time.
134
+ - Use AGENTS.md instead when CLAUDE.md is absent.
135
+ - If instructed to use "haiku agent", "sonnet agent", or "opus agent", use "worker" instead.`;
package/src/subagent.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
  import fs from "node:fs/promises";
8
8
  import path from "node:path";
9
9
  import { AGENT_PROJECT_METADATA_DIR } from "./env.mjs";
10
+ import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "./prompt.mjs";
10
11
  import { reportAsSubagentToolName } from "./tools/reportAsSubagent.mjs";
11
12
 
12
13
  /** @typedef {ReturnType<typeof createSubagentManager>} SubagentManager */
@@ -73,7 +74,9 @@ export function createSubagentManager(agentRoles, handlers) {
73
74
  error: `Agent role "${name}" not found. Available agent roles:\n${availableRoles}\n\nTo use an ad-hoc role, prefix the name with "custom:" (e.g., "custom:researcher").`,
74
75
  };
75
76
  }
76
- roleContent = role.content;
77
+ roleContent = role.claudeOriginated
78
+ ? `${role.content}\n\n---\n\n${CLAUDE_CODE_COMPATIBILITY_NOTES}`
79
+ : role.content;
77
80
  }
78
81
 
79
82
  subagents.push({