@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iinm/plain-agent",
3
- "version": "1.9.0",
3
+ "version": "1.9.1",
4
4
  "description": "A lightweight CLI-based coding agent",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 line = ` ${styleText("cyan", role.id.padEnd(20))} - ${role.description}`;
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 line = ` ${styleText("cyan", prompt.id.padEnd(20))} - ${prompt.description}`;
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
  );
@@ -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 desc =
32
- role.description.length > 100
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 desc =
42
- skill.description.length > 100
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
- * Only supports `key: value` format. No multiline strings.
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
- for (const line of frontmatter.split(/\r?\n/)) {
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
+ }