@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.
- package/.config/config.predefined.json +6 -6
- package/package.json +4 -4
- package/src/cliCommands.mjs +270 -0
- package/src/cliCompleter.mjs +222 -0
- package/src/cliFormatter.mjs +63 -1
- package/src/cliInteractive.mjs +72 -501
- package/src/cliPasteTransform.mjs +128 -0
- package/src/context/loadAgentRoles.mjs +5 -0
- package/src/context/loadPrompts.mjs +5 -0
- package/src/prompt.mjs +7 -6
- package/src/subagent.mjs +4 -1
|
@@ -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.
|
|
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({
|