@iinm/plain-agent 1.9.0 → 1.9.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/README.md +4 -0
- package/package.json +1 -1
- package/src/cliCommands.mjs +5 -2
- package/src/cliCompleter.mjs +2 -1
- package/src/prompt.mjs +7 -16
- package/src/utils/parseFrontmatter.mjs +65 -2
- package/src/utils/toOneLine.mjs +11 -0
package/README.md
CHANGED
|
@@ -16,6 +16,8 @@ A lightweight CLI-based coding agent.
|
|
|
16
16
|
- **Sandboxed execution** — Run the agent's shell commands inside a Docker
|
|
17
17
|
container with network access restricted to allowlisted destinations
|
|
18
18
|
(e.g., `registry.npmjs.org` only for `npm install`).
|
|
19
|
+
- **Plain-text memory** — Task state is persisted as Markdown files under
|
|
20
|
+
`.plain-agent/memory/`, easy to review.
|
|
19
21
|
- **Extensible** — Define prompts and subagents in Markdown. Connect MCP servers.
|
|
20
22
|
Supports Claude Code plugins and `.claude/` commands, subagents, and skills.
|
|
21
23
|
|
|
@@ -24,6 +26,7 @@ A lightweight CLI-based coding agent.
|
|
|
24
26
|
- **Sequential subagent execution** — Subagents run one at a time rather than
|
|
25
27
|
in parallel. The trade-off is full visibility: every step is streamed to
|
|
26
28
|
your terminal so you can follow exactly what each subagent is doing.
|
|
29
|
+
- **No session persistence** — Sessions are not persisted. Start a fresh session and use memory files (`.plain-agent/memory/`) instead.
|
|
27
30
|
|
|
28
31
|
## Requirements
|
|
29
32
|
|
|
@@ -495,6 +498,7 @@ Files are loaded in the following order. Settings in later files override earlie
|
|
|
495
498
|
|
|
496
499
|
The agent can use the following tools to assist with tasks:
|
|
497
500
|
|
|
501
|
+
- **read_file**: Read a file with line numbers (1-indexed). Supports `offset` and `limit` to read a specific range.
|
|
498
502
|
- **write_file**: Write a file.
|
|
499
503
|
- **patch_file**: Patch a file.
|
|
500
504
|
- **exec_command**: Run a command without shell interpretation.
|
package/package.json
CHANGED
package/src/cliCommands.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import { loadUserMessageContext } from "./context/loadUserMessageContext.mjs";
|
|
|
12
12
|
import { CLAUDE_CODE_COMPATIBILITY_NOTES } from "./prompt.mjs";
|
|
13
13
|
import { parseFileRange } from "./utils/parseFileRange.mjs";
|
|
14
14
|
import { readFileRange } from "./utils/readFileRange.mjs";
|
|
15
|
+
import { toOneLine } from "./utils/toOneLine.mjs";
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @typedef {"prompt" | "continue"} CommandResult
|
|
@@ -172,7 +173,8 @@ export function createCommandHandler({
|
|
|
172
173
|
} else {
|
|
173
174
|
for (const role of agentRoles.values()) {
|
|
174
175
|
const maxLength = process.stdout.columns ?? 100;
|
|
175
|
-
const
|
|
176
|
+
const desc = toOneLine(role.description);
|
|
177
|
+
const line = ` ${styleText("cyan", role.id.padEnd(20))} - ${desc}`;
|
|
176
178
|
console.log(
|
|
177
179
|
line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
|
|
178
180
|
);
|
|
@@ -201,7 +203,8 @@ export function createCommandHandler({
|
|
|
201
203
|
} else {
|
|
202
204
|
for (const prompt of prompts.values()) {
|
|
203
205
|
const maxLength = process.stdout.columns ?? 100;
|
|
204
|
-
const
|
|
206
|
+
const desc = toOneLine(prompt.description);
|
|
207
|
+
const line = ` ${styleText("cyan", prompt.id.padEnd(20))} - ${desc}`;
|
|
205
208
|
console.log(
|
|
206
209
|
line.length > maxLength ? `${line.slice(0, maxLength)}...` : line,
|
|
207
210
|
);
|
package/src/cliCompleter.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { styleText } from "node:util";
|
|
6
6
|
import { loadAgentRoles } from "./context/loadAgentRoles.mjs";
|
|
7
7
|
import { loadPrompts } from "./context/loadPrompts.mjs";
|
|
8
|
+
import { toOneLine } from "./utils/toOneLine.mjs";
|
|
8
9
|
|
|
9
10
|
// Define available slash commands for tab completion
|
|
10
11
|
export const SLASH_COMMANDS = [
|
|
@@ -129,7 +130,7 @@ function showCompletions(rl, candidates, line, callback) {
|
|
|
129
130
|
if (typeof c === "string") return c;
|
|
130
131
|
const nameText = c.name.padEnd(25);
|
|
131
132
|
const separator = " - ";
|
|
132
|
-
const descText = c.description;
|
|
133
|
+
const descText = toOneLine(c.description);
|
|
133
134
|
|
|
134
135
|
// 画面幅に合わせて説明文をカット(色を付ける前に計算)
|
|
135
136
|
const availableWidth =
|
package/src/prompt.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toOneLine } from "./utils/toOneLine.mjs";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @typedef {object} PromptConfig
|
|
3
5
|
* @property {string} username
|
|
@@ -28,37 +30,27 @@ export function createPrompt({
|
|
|
28
30
|
}) {
|
|
29
31
|
const agentRoleDescriptions = Array.from(agentRoles.entries())
|
|
30
32
|
.map(([id, role]) => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
? `${role.description.substring(0, 100)}...`
|
|
34
|
-
: role.description;
|
|
33
|
+
const flat = toOneLine(role.description);
|
|
34
|
+
const desc = flat.length > 100 ? `${flat.substring(0, 100)}...` : flat;
|
|
35
35
|
return `- ${id}: ${desc}`;
|
|
36
36
|
})
|
|
37
37
|
.join("\n");
|
|
38
38
|
|
|
39
39
|
const skillDescriptions = skills
|
|
40
40
|
.map((skill) => {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
? `${skill.description.substring(0, 100)}...`
|
|
44
|
-
: skill.description;
|
|
41
|
+
const flat = toOneLine(skill.description);
|
|
42
|
+
const desc = flat.length > 100 ? `${flat.substring(0, 100)}...` : flat;
|
|
45
43
|
return `- ${skill.filePath}\n ${desc}`;
|
|
46
44
|
})
|
|
47
45
|
.join("\n");
|
|
48
46
|
|
|
49
47
|
return `
|
|
50
|
-
# Communication Style
|
|
51
|
-
|
|
52
|
-
- Respond in the user's language.
|
|
53
|
-
- Address the user by their name, rather than "user".
|
|
54
|
-
- Use emojis sparingly to highlight key points.
|
|
55
|
-
|
|
56
48
|
# Memory Files
|
|
57
49
|
|
|
58
50
|
- Create/Update memory files after creating/updating a plan, completing milestones, encountering issues, or making decisions.
|
|
59
51
|
- Update existing task memory when continuing the same task.
|
|
60
|
-
- Write the memory content in the user's language.
|
|
61
52
|
- Ensure self-containment: The file must be standalone. A reader should fully understand the task context, logic and progress without any other references.
|
|
53
|
+
- Write the memory content in the user's language.
|
|
62
54
|
|
|
63
55
|
Memory files should include:
|
|
64
56
|
- Task overview: What the task is, why it's being done, requirements and constraints
|
|
@@ -88,7 +80,6 @@ Examples:
|
|
|
88
80
|
|
|
89
81
|
- Only use when the user explicitly requests it.
|
|
90
82
|
- Create a new session with the given tmux session id.
|
|
91
|
-
- Use relative paths.
|
|
92
83
|
|
|
93
84
|
Examples:
|
|
94
85
|
- Start session: new-session ["-d", "-s", "<tmux-session-id>"]
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Parse simple key-value frontmatter using regex.
|
|
3
|
-
*
|
|
3
|
+
* Supports `key: value` and YAML block scalars (`key: |` literal,
|
|
4
|
+
* `key: >` folded), with optional chomping indicators (`-`, `+`).
|
|
5
|
+
* Block scalar lines are read while indented further than column 0,
|
|
6
|
+
* using the first non-empty block line's indentation as the base.
|
|
4
7
|
* @param {string} frontmatter - The YAML frontmatter content (without --- delimiters)
|
|
5
8
|
* @returns {Record<string, string>} Parsed key-value pairs
|
|
6
9
|
*/
|
|
@@ -8,12 +11,72 @@ export function parseFrontmatter(frontmatter) {
|
|
|
8
11
|
/** @type {Record<string, string>} */
|
|
9
12
|
const result = {};
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
15
|
+
let i = 0;
|
|
16
|
+
while (i < lines.length) {
|
|
17
|
+
const line = lines[i];
|
|
18
|
+
|
|
19
|
+
const blockMatch = line.match(/^(\w[\w-]*):\s*([|>])[+-]?\s*$/);
|
|
20
|
+
if (blockMatch) {
|
|
21
|
+
const key = blockMatch[1];
|
|
22
|
+
const style = blockMatch[2];
|
|
23
|
+
i++;
|
|
24
|
+
|
|
25
|
+
/** @type {string[]} */
|
|
26
|
+
const blockLines = [];
|
|
27
|
+
let indent = -1;
|
|
28
|
+
while (i < lines.length) {
|
|
29
|
+
const blockLine = lines[i];
|
|
30
|
+
if (blockLine.trim() === "") {
|
|
31
|
+
blockLines.push("");
|
|
32
|
+
i++;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const leadingSpaces = (blockLine.match(/^( *)/)?.[1] ?? "").length;
|
|
36
|
+
if (indent === -1) {
|
|
37
|
+
if (leadingSpaces === 0) break;
|
|
38
|
+
indent = leadingSpaces;
|
|
39
|
+
} else if (leadingSpaces < indent) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
blockLines.push(blockLine.slice(indent));
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
while (blockLines.at(-1) === "") {
|
|
47
|
+
blockLines.pop();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
result[key] =
|
|
51
|
+
style === "|" ? blockLines.join("\n") : foldFolded(blockLines);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
12
55
|
const match = line.match(/^(\w[\w-]*):\s?(.*)$/);
|
|
13
56
|
if (match) {
|
|
14
57
|
result[match[1]] = match[2].trimEnd();
|
|
15
58
|
}
|
|
59
|
+
i++;
|
|
16
60
|
}
|
|
17
61
|
|
|
18
62
|
return result;
|
|
19
63
|
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {string[]} blockLines
|
|
67
|
+
* @returns {string}
|
|
68
|
+
*/
|
|
69
|
+
function foldFolded(blockLines) {
|
|
70
|
+
const paragraphs = [];
|
|
71
|
+
let current = [];
|
|
72
|
+
for (const line of blockLines) {
|
|
73
|
+
if (line === "") {
|
|
74
|
+
paragraphs.push(current.join(" "));
|
|
75
|
+
current = [];
|
|
76
|
+
} else {
|
|
77
|
+
current.push(line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
paragraphs.push(current.join(" "));
|
|
81
|
+
return paragraphs.join("\n");
|
|
82
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collapse newlines (and any whitespace adjacent to them) into a single space
|
|
3
|
+
* and trim. Used for rendering frontmatter values such as `description` in
|
|
4
|
+
* single-line UI contexts (bullet lists, completer rows, console output).
|
|
5
|
+
*
|
|
6
|
+
* @param {string} s
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function toOneLine(s) {
|
|
10
|
+
return s.replace(/\s*\n\s*/g, " ").trim();
|
|
11
|
+
}
|