octto 0.2.0 → 0.3.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.
package/README.md CHANGED
@@ -117,6 +117,34 @@ Optional `~/.config/opencode/octto.json`:
117
117
  |--------|------|---------|-------------|
118
118
  | `port` | number | `0` (random) | Fixed port for the browser UI server |
119
119
  | `agents` | object | - | Override agent models/settings |
120
+ | `fragments` | object | - | Custom instructions injected into agent prompts |
121
+
122
+ ### Fragments
123
+
124
+ Inject custom instructions into agent prompts. Useful for customizing agent behavior per-project or globally.
125
+
126
+ **Global config** (`~/.config/opencode/octto.json`):
127
+
128
+ ```json
129
+ {
130
+ "fragments": {
131
+ "octto": ["Always suggest 3 implementation approaches"],
132
+ "probe": ["Include emoji in every question"],
133
+ "bootstrapper": ["Focus on technical feasibility"]
134
+ }
135
+ }
136
+ ```
137
+
138
+ **Project config** (`.octto/fragments.json` in your project root):
139
+
140
+ ```json
141
+ {
142
+ "octto": ["This project uses React - focus on component patterns"],
143
+ "probe": ["Ask about testing strategy for each feature"]
144
+ }
145
+ ```
146
+
147
+ Fragments are merged: global fragments load first, project fragments append. Each fragment becomes a bullet point in a `<user-instructions>` block prepended to the agent's system prompt.
120
148
 
121
149
  ### Environment Variables
122
150
 
@@ -1,3 +1,3 @@
1
- export type { AgentOverride, CustomConfig, OcttoConfig } from "./loader";
1
+ export type { AgentOverride, CustomConfig, Fragments, OcttoConfig } from "./loader";
2
2
  export { loadCustomConfig, resolvePort } from "./loader";
3
- export { AgentOverrideSchema, OcttoConfigSchema, PortSchema } from "./schema";
3
+ export { AgentOverrideSchema, FragmentsSchema, OcttoConfigSchema, PortSchema } from "./schema";
@@ -1,6 +1,7 @@
1
1
  import type { AgentConfig } from "@opencode-ai/sdk";
2
2
  import { AGENTS } from "@/agents";
3
- export type { AgentOverride, OcttoConfig } from "./schema";
3
+ import { type Fragments } from "./schema";
4
+ export type { AgentOverride, Fragments, OcttoConfig } from "./schema";
4
5
  /**
5
6
  * Resolve port from environment variable or config.
6
7
  * Priority: OCTTO_PORT env var > config port > default (0 = random)
@@ -9,6 +10,7 @@ export declare function resolvePort(configPort?: number): number;
9
10
  export interface CustomConfig {
10
11
  agents: Record<AGENTS, AgentConfig>;
11
12
  port: number;
13
+ fragments: Fragments;
12
14
  }
13
15
  /**
14
16
  * Load user configuration and merge with plugin agents.
@@ -39,6 +39,7 @@ export declare const AgentOverrideSchema: Omit<v.ObjectSchema<{
39
39
  } | undefined;
40
40
  };
41
41
  export declare const PortSchema: v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 65535, undefined>]>;
42
+ export declare const FragmentsSchema: v.OptionalSchema<v.RecordSchema<v.EnumSchema<typeof AGENTS, undefined>, v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
42
43
  export declare const OcttoConfigSchema: v.ObjectSchema<{
43
44
  readonly agents: v.OptionalSchema<v.RecordSchema<v.EnumSchema<typeof AGENTS, undefined>, Omit<v.ObjectSchema<{
44
45
  readonly model: v.StringSchema<undefined>;
@@ -79,6 +80,8 @@ export declare const OcttoConfigSchema: v.ObjectSchema<{
79
80
  } | undefined;
80
81
  }, undefined>, undefined>;
81
82
  readonly port: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 0, undefined>, v.MaxValueAction<number, 65535, undefined>]>, undefined>;
83
+ readonly fragments: v.OptionalSchema<v.RecordSchema<v.EnumSchema<typeof AGENTS, undefined>, v.ArraySchema<v.StringSchema<undefined>, undefined>, undefined>, undefined>;
82
84
  }, undefined>;
83
85
  export type AgentOverride = v.InferOutput<typeof AgentOverrideSchema>;
86
+ export type Fragments = v.InferOutput<typeof FragmentsSchema>;
84
87
  export type OcttoConfig = v.InferOutput<typeof OcttoConfigSchema>;
@@ -0,0 +1,37 @@
1
+ type FragmentsRecord = Record<string, string[]> | undefined;
2
+ /**
3
+ * Format fragments array as an XML block to prepend to agent prompts.
4
+ */
5
+ export declare function formatFragmentsBlock(fragments: string[] | undefined): string;
6
+ /**
7
+ * Merge global and project fragments.
8
+ * Global fragments come first, project fragments append.
9
+ */
10
+ export declare function mergeFragments(global: FragmentsRecord, project: FragmentsRecord): Record<string, string[]>;
11
+ /**
12
+ * Load project-level fragments from .octto/fragments.json
13
+ */
14
+ export declare function loadProjectFragments(projectDir: string): Promise<Record<string, string[]> | undefined>;
15
+ /**
16
+ * Calculate Levenshtein distance between two strings.
17
+ * Used for suggesting similar agent names for typos.
18
+ */
19
+ export declare function levenshteinDistance(a: string, b: string): number;
20
+ /**
21
+ * Warn about unknown agent names in fragments config.
22
+ * Suggests similar valid agent names for likely typos.
23
+ */
24
+ export declare function warnUnknownAgents(fragments: Record<string, string[]> | undefined): void;
25
+ export interface FragmentInjectorContext {
26
+ projectDir: string;
27
+ }
28
+ /**
29
+ * Create a fragment injector that can modify agent system prompts.
30
+ * Returns merged fragments from global config and project config.
31
+ */
32
+ export declare function createFragmentInjector(ctx: FragmentInjectorContext, globalFragments: FragmentsRecord): Promise<Record<string, string[]>>;
33
+ /**
34
+ * Get the system prompt prefix for a specific agent.
35
+ */
36
+ export declare function getAgentSystemPromptPrefix(fragments: Record<string, string[]>, agentName: string): string;
37
+ export {};
@@ -0,0 +1 @@
1
+ export { createFragmentInjector, type FragmentInjectorContext, formatFragmentsBlock, getAgentSystemPromptPrefix, levenshteinDistance, loadProjectFragments, mergeFragments, warnUnknownAgents, } from "./fragment-injector";
package/dist/index.js CHANGED
@@ -509,6 +509,58 @@ function getFallback(schema, dataset, config$1) {
509
509
  function getDefault(schema, dataset, config$1) {
510
510
  return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
511
511
  }
512
+ function array(item, message$1) {
513
+ return {
514
+ kind: "schema",
515
+ type: "array",
516
+ reference: array,
517
+ expects: "Array",
518
+ async: false,
519
+ item,
520
+ message: message$1,
521
+ get "~standard"() {
522
+ return /* @__PURE__ */ _getStandardProps(this);
523
+ },
524
+ "~run"(dataset, config$1) {
525
+ const input = dataset.value;
526
+ if (Array.isArray(input)) {
527
+ dataset.typed = true;
528
+ dataset.value = [];
529
+ for (let key = 0;key < input.length; key++) {
530
+ const value$1 = input[key];
531
+ const itemDataset = this.item["~run"]({ value: value$1 }, config$1);
532
+ if (itemDataset.issues) {
533
+ const pathItem = {
534
+ type: "array",
535
+ origin: "value",
536
+ input,
537
+ key,
538
+ value: value$1
539
+ };
540
+ for (const issue of itemDataset.issues) {
541
+ if (issue.path)
542
+ issue.path.unshift(pathItem);
543
+ else
544
+ issue.path = [pathItem];
545
+ dataset.issues?.push(issue);
546
+ }
547
+ if (!dataset.issues)
548
+ dataset.issues = itemDataset.issues;
549
+ if (config$1.abortEarly) {
550
+ dataset.typed = false;
551
+ break;
552
+ }
553
+ }
554
+ if (!itemDataset.typed)
555
+ dataset.typed = false;
556
+ dataset.value.push(itemDataset.value);
557
+ }
558
+ } else
559
+ _addIssue(this, "type", dataset, config$1);
560
+ return dataset;
561
+ }
562
+ };
563
+ }
512
564
  function enum_(enum__, message$1) {
513
565
  const options = [];
514
566
  for (const key in enum__)
@@ -796,9 +848,11 @@ var AgentOverrideSchema = partial(object({
796
848
  maxSteps: pipe(number(), integer(), minValue(1))
797
849
  }));
798
850
  var PortSchema = pipe(number(), integer(), minValue(0), maxValue(65535));
851
+ var FragmentsSchema = optional(record(enum_(AGENTS), array(string())));
799
852
  var OcttoConfigSchema = object({
800
853
  agents: optional(record(enum_(AGENTS), AgentOverrideSchema)),
801
- port: optional(PortSchema)
854
+ port: optional(PortSchema),
855
+ fragments: FragmentsSchema
802
856
  });
803
857
 
804
858
  // src/config/loader.ts
@@ -880,9 +934,117 @@ async function loadCustomConfig(agents2, configDir) {
880
934
  }
881
935
  return {
882
936
  agents: mergedAgents,
883
- port: resolvePort(config?.port)
937
+ port: resolvePort(config?.port),
938
+ fragments: config?.fragments
884
939
  };
885
940
  }
941
+ // src/hooks/fragment-injector.ts
942
+ import { readFile as readFile2 } from "fs/promises";
943
+ import { join as join2 } from "path";
944
+ var VALID_AGENT_NAMES2 = Object.values(AGENTS);
945
+ var ProjectFragmentsSchema = record(string(), array(string()));
946
+ function formatFragmentsBlock(fragments) {
947
+ if (!fragments || fragments.length === 0) {
948
+ return "";
949
+ }
950
+ const bulletPoints = fragments.map((f) => `- ${f}`).join(`
951
+ `);
952
+ return `<user-instructions>
953
+ ${bulletPoints}
954
+ </user-instructions>
955
+
956
+ `;
957
+ }
958
+ function mergeFragments(global, project) {
959
+ const result = {};
960
+ if (global) {
961
+ for (const [agent4, frags] of Object.entries(global)) {
962
+ result[agent4] = [...frags];
963
+ }
964
+ }
965
+ if (project) {
966
+ for (const [agent4, frags] of Object.entries(project)) {
967
+ if (result[agent4]) {
968
+ result[agent4].push(...frags);
969
+ } else {
970
+ result[agent4] = [...frags];
971
+ }
972
+ }
973
+ }
974
+ return result;
975
+ }
976
+ async function loadProjectFragments(projectDir) {
977
+ const fragmentsPath = join2(projectDir, ".octto", "fragments.json");
978
+ try {
979
+ const content = await readFile2(fragmentsPath, "utf-8");
980
+ const parsed = JSON.parse(content);
981
+ const result = safeParse(ProjectFragmentsSchema, parsed);
982
+ if (!result.success) {
983
+ console.warn(`[octto] Invalid fragments.json schema in ${fragmentsPath}`);
984
+ return;
985
+ }
986
+ return result.output;
987
+ } catch {
988
+ return;
989
+ }
990
+ }
991
+ function levenshteinDistance(a, b) {
992
+ if (a.length === 0)
993
+ return b.length;
994
+ if (b.length === 0)
995
+ return a.length;
996
+ const matrix = [];
997
+ for (let i = 0;i <= b.length; i++) {
998
+ matrix[i] = [i];
999
+ }
1000
+ for (let j = 0;j <= a.length; j++) {
1001
+ matrix[0][j] = j;
1002
+ }
1003
+ for (let i = 1;i <= b.length; i++) {
1004
+ for (let j = 1;j <= a.length; j++) {
1005
+ if (b.charAt(i - 1) === a.charAt(j - 1)) {
1006
+ matrix[i][j] = matrix[i - 1][j - 1];
1007
+ } else {
1008
+ matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1);
1009
+ }
1010
+ }
1011
+ }
1012
+ return matrix[b.length][a.length];
1013
+ }
1014
+ function warnUnknownAgents(fragments) {
1015
+ if (!fragments)
1016
+ return;
1017
+ for (const agentName of Object.keys(fragments)) {
1018
+ if (VALID_AGENT_NAMES2.includes(agentName)) {
1019
+ continue;
1020
+ }
1021
+ let closest;
1022
+ let minDistance = Infinity;
1023
+ for (const validName of VALID_AGENT_NAMES2) {
1024
+ const distance = levenshteinDistance(agentName, validName);
1025
+ if (distance < minDistance && distance <= 3) {
1026
+ minDistance = distance;
1027
+ closest = validName;
1028
+ }
1029
+ }
1030
+ let message = `[octto] Unknown agent "${agentName}" in fragments config.`;
1031
+ if (closest) {
1032
+ message += ` Did you mean "${closest}"?`;
1033
+ }
1034
+ message += ` Valid agents: ${VALID_AGENT_NAMES2.join(", ")}`;
1035
+ console.warn(message);
1036
+ }
1037
+ }
1038
+ async function createFragmentInjector(ctx, globalFragments) {
1039
+ const projectFragments = await loadProjectFragments(ctx.projectDir);
1040
+ const merged = mergeFragments(globalFragments, projectFragments);
1041
+ warnUnknownAgents(globalFragments);
1042
+ warnUnknownAgents(projectFragments);
1043
+ return merged;
1044
+ }
1045
+ function getAgentSystemPromptPrefix(fragments, agentName) {
1046
+ return formatFragmentsBlock(fragments[agentName]);
1047
+ }
886
1048
  // src/constants.ts
887
1049
  var DEFAULT_ANSWER_TIMEOUT_MS = 300000;
888
1050
 
@@ -3101,7 +3263,7 @@ __export(exports_external, {
3101
3263
  bigint: () => bigint2,
3102
3264
  base64url: () => base64url2,
3103
3265
  base64: () => base642,
3104
- array: () => array,
3266
+ array: () => array2,
3105
3267
  any: () => any,
3106
3268
  _function: () => _function,
3107
3269
  _default: () => _default2,
@@ -3597,8 +3759,8 @@ function getEnumValues(entries) {
3597
3759
  const values = Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
3598
3760
  return values;
3599
3761
  }
3600
- function joinValues(array, separator = "|") {
3601
- return array.map((val) => stringifyPrimitive(val)).join(separator);
3762
+ function joinValues(array2, separator = "|") {
3763
+ return array2.map((val) => stringifyPrimitive(val)).join(separator);
3602
3764
  }
3603
3765
  function jsonStringifyReplacer(_, value) {
3604
3766
  if (typeof value === "bigint")
@@ -14286,7 +14448,7 @@ var ZodType = /* @__PURE__ */ $constructor("ZodType", (inst, def) => {
14286
14448
  inst.nullable = () => nullable(inst);
14287
14449
  inst.nullish = () => optional2(nullable(inst));
14288
14450
  inst.nonoptional = (params) => nonoptional(inst, params);
14289
- inst.array = () => array(inst);
14451
+ inst.array = () => array2(inst);
14290
14452
  inst.or = (arg) => union([inst, arg]);
14291
14453
  inst.and = (arg) => intersection(inst, arg);
14292
14454
  inst.transform = (tx) => pipe2(inst, transform(tx));
@@ -14707,7 +14869,7 @@ var ZodArray = /* @__PURE__ */ $constructor("ZodArray", (inst, def) => {
14707
14869
  inst.length = (len, params) => inst.check(_length(len, params));
14708
14870
  inst.unwrap = () => inst.element;
14709
14871
  });
14710
- function array(element, params) {
14872
+ function array2(element, params) {
14711
14873
  return _array(ZodArray, element, params);
14712
14874
  }
14713
14875
  function keyof(schema) {
@@ -15169,7 +15331,7 @@ var ZodFunction = /* @__PURE__ */ $constructor("ZodFunction", (inst, def) => {
15169
15331
  function _function(params) {
15170
15332
  return new ZodFunction({
15171
15333
  type: "function",
15172
- input: Array.isArray(params?.input) ? tuple(params?.input) : params?.input ?? array(unknown()),
15334
+ input: Array.isArray(params?.input) ? tuple(params?.input) : params?.input ?? array2(unknown()),
15173
15335
  output: params?.output ?? unknown()
15174
15336
  });
15175
15337
  }
@@ -15213,7 +15375,7 @@ var stringbool = (...args) => _stringbool({
15213
15375
  }, ...args);
15214
15376
  function json(params) {
15215
15377
  const jsonSchema = lazy(() => {
15216
- return union([string3(params), number3(), boolean2(), _null3(), array(jsonSchema), record2(string3(), jsonSchema)]);
15378
+ return union([string3(params), number3(), boolean2(), _null3(), array2(jsonSchema), record2(string3(), jsonSchema)]);
15217
15379
  });
15218
15380
  return jsonSchema;
15219
15381
  }
@@ -15279,7 +15441,7 @@ tool.schema = exports_external;
15279
15441
 
15280
15442
  // src/state/persistence.ts
15281
15443
  import { existsSync, mkdirSync, readdirSync, rmSync } from "fs";
15282
- import { join as join2 } from "path";
15444
+ import { join as join3 } from "path";
15283
15445
  function validateSessionId(sessionId) {
15284
15446
  if (!/^[a-zA-Z0-9_-]+$/.test(sessionId)) {
15285
15447
  throw new Error(`Invalid session ID: ${sessionId}`);
@@ -15288,7 +15450,7 @@ function validateSessionId(sessionId) {
15288
15450
  function createStatePersistence(baseDir = ".brainstorm") {
15289
15451
  function getFilePath(sessionId) {
15290
15452
  validateSessionId(sessionId);
15291
- return join2(baseDir, `${sessionId}.json`);
15453
+ return join3(baseDir, `${sessionId}.json`);
15292
15454
  }
15293
15455
  function ensureDir() {
15294
15456
  if (!existsSync(baseDir)) {
@@ -16539,8 +16701,16 @@ function createOcttoTools(sessions, client) {
16539
16701
  }
16540
16702
 
16541
16703
  // src/index.ts
16542
- var Octto = async ({ client }) => {
16704
+ var Octto = async ({ client, directory }) => {
16543
16705
  const customConfig = await loadCustomConfig(agents);
16706
+ const fragments = await createFragmentInjector({ projectDir: directory }, customConfig.fragments);
16707
+ for (const agentName of Object.values(AGENTS)) {
16708
+ const prefix = getAgentSystemPromptPrefix(fragments, agentName);
16709
+ if (prefix && customConfig.agents[agentName]?.prompt) {
16710
+ customConfig.agents[agentName].prompt = prefix + customConfig.agents[agentName].prompt;
16711
+ }
16712
+ }
16713
+ warnUnknownAgents(customConfig.fragments);
16544
16714
  const sessions = createSessionStore({ port: customConfig.port });
16545
16715
  const tracked = new Map;
16546
16716
  const tools = createOcttoTools(sessions, client);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octto",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "OpenCode plugin that turns rough ideas into designs through branch-based exploration",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",