@powerhousedao/ph-clint-cli 0.1.0-dev.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 +84 -0
- package/dist/agents/clint-agent.d.ts +12 -0
- package/dist/agents/clint-agent.js +42 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +79 -0
- package/dist/codegen/builders/agent-base-md.d.ts +5 -0
- package/dist/codegen/builders/agent-base-md.js +18 -0
- package/dist/codegen/builders/agent-ts.d.ts +9 -0
- package/dist/codegen/builders/agent-ts.js +34 -0
- package/dist/codegen/builders/app-index-ts.d.ts +18 -0
- package/dist/codegen/builders/app-index-ts.js +33 -0
- package/dist/codegen/builders/build-skills-ts.d.ts +4 -0
- package/dist/codegen/builders/build-skills-ts.js +37 -0
- package/dist/codegen/builders/cli-package-json.d.ts +8 -0
- package/dist/codegen/builders/cli-package-json.js +69 -0
- package/dist/codegen/builders/cli-ts.d.ts +7 -0
- package/dist/codegen/builders/cli-ts.js +110 -0
- package/dist/codegen/builders/config-ts.d.ts +8 -0
- package/dist/codegen/builders/config-ts.js +21 -0
- package/dist/codegen/builders/eslint-config-js.d.ts +4 -0
- package/dist/codegen/builders/eslint-config-js.js +13 -0
- package/dist/codegen/builders/framework-gen-ts.d.ts +20 -0
- package/dist/codegen/builders/framework-gen-ts.js +62 -0
- package/dist/codegen/builders/framework-ts.d.ts +21 -0
- package/dist/codegen/builders/framework-ts.js +74 -0
- package/dist/codegen/builders/gitignore.d.ts +4 -0
- package/dist/codegen/builders/gitignore.js +18 -0
- package/dist/codegen/builders/index.d.ts +43 -0
- package/dist/codegen/builders/index.js +60 -0
- package/dist/codegen/builders/jest-config-js.d.ts +4 -0
- package/dist/codegen/builders/jest-config-js.js +19 -0
- package/dist/codegen/builders/main-ts.d.ts +5 -0
- package/dist/codegen/builders/main-ts.js +13 -0
- package/dist/codegen/builders/mastra-index-ts.d.ts +8 -0
- package/dist/codegen/builders/mastra-index-ts.js +30 -0
- package/dist/codegen/builders/readme-md.d.ts +5 -0
- package/dist/codegen/builders/readme-md.js +44 -0
- package/dist/codegen/builders/root-package-json.d.ts +7 -0
- package/dist/codegen/builders/root-package-json.js +27 -0
- package/dist/codegen/builders/tsconfig-json.d.ts +4 -0
- package/dist/codegen/builders/tsconfig-json.js +22 -0
- package/dist/codegen/exec.d.ts +20 -0
- package/dist/codegen/exec.js +41 -0
- package/dist/codegen/hashes.d.ts +6 -0
- package/dist/codegen/hashes.js +60 -0
- package/dist/codegen/index.d.ts +39 -0
- package/dist/codegen/index.js +292 -0
- package/dist/codegen/markers.d.ts +36 -0
- package/dist/codegen/markers.js +86 -0
- package/dist/codegen/migrate/flat-to-split.d.ts +15 -0
- package/dist/codegen/migrate/flat-to-split.js +119 -0
- package/dist/codegen/scaffold.d.ts +34 -0
- package/dist/codegen/scaffold.js +60 -0
- package/dist/codegen/write.d.ts +6 -0
- package/dist/codegen/write.js +32 -0
- package/dist/commands/init.d.ts +57 -0
- package/dist/commands/init.js +168 -0
- package/dist/commands/regen.d.ts +18 -0
- package/dist/commands/regen.js +68 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.js +7 -0
- package/dist/framework.d.ts +239 -0
- package/dist/framework.gen.d.ts +37 -0
- package/dist/framework.gen.js +16 -0
- package/dist/framework.js +41 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +3 -0
- package/dist/mastra/index.d.ts +10 -0
- package/dist/mastra/index.js +11 -0
- package/dist/spec/file.d.ts +4 -0
- package/dist/spec/file.js +34 -0
- package/dist/spec/from-document.d.ts +18 -0
- package/dist/spec/from-document.js +30 -0
- package/dist/spec/types.d.ts +62 -0
- package/dist/spec/types.js +112 -0
- package/dist/triggers/spec-change.d.ts +43 -0
- package/dist/triggers/spec-change.js +171 -0
- package/package.json +60 -0
- package/prompts/agent-profiles/AgentBase.md +13 -0
- package/prompts/skills-ext/.gitkeep +0 -0
- package/prompts/skills-tpl/.gitkeep +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THIS FILE IS GENERATED BY ph-clint-cli. DO NOT EDIT.
|
|
3
|
+
*
|
|
4
|
+
* Regenerated on every `ph-clint regen` and on every spec-change tick.
|
|
5
|
+
* Put custom bindings in `framework.ts`, not here — edits to this file
|
|
6
|
+
* will be overwritten.
|
|
7
|
+
*/
|
|
8
|
+
import { defineRegistry } from '@powerhousedao/ph-clint';
|
|
9
|
+
import { PhClintProject } from '@powerhousedao/ph-clint-app';
|
|
10
|
+
/**
|
|
11
|
+
* Typed document registry derived from the reactor package's document models.
|
|
12
|
+
* Regenerated whenever the spec's `documentTypes` list changes.
|
|
13
|
+
*/
|
|
14
|
+
export const registry = defineRegistry([
|
|
15
|
+
PhClintProject,
|
|
16
|
+
]);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-CLI typed framework binding.
|
|
3
|
+
*
|
|
4
|
+
* This file is USER-OWNED. ph-clint-cli emits it once at project creation
|
|
5
|
+
* and never overwrites it afterwards — edit `configSchema` and
|
|
6
|
+
* `secretsSchema` freely.
|
|
7
|
+
*
|
|
8
|
+
* Typed factories (`defineCommand`, `defineTrigger`, ...) and the document
|
|
9
|
+
* `registry` come from `framework.gen.ts`, which IS regenerated whenever
|
|
10
|
+
* the spec's `documentTypes` list changes.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
import { createTypes } from '@powerhousedao/ph-clint';
|
|
14
|
+
import { registry } from './framework.gen.js';
|
|
15
|
+
export const configSchema = z.object({
|
|
16
|
+
model: z
|
|
17
|
+
.string()
|
|
18
|
+
.default('anthropic/claude-haiku-4-5')
|
|
19
|
+
.describe('LLM model to use'),
|
|
20
|
+
devServicePort: z
|
|
21
|
+
.number()
|
|
22
|
+
.default(3000)
|
|
23
|
+
.describe('Port reserved for the impl project (Service B) `pnpm dev`'),
|
|
24
|
+
phVersion: z
|
|
25
|
+
.string()
|
|
26
|
+
.default('6.0.0-dev.170')
|
|
27
|
+
.describe('Pinned Powerhouse version used when emitting a new impl project'),
|
|
28
|
+
projectDocumentId: z
|
|
29
|
+
.string()
|
|
30
|
+
.optional()
|
|
31
|
+
.describe('ID of the powerhouse/ph-clint-project document that drives codegen (set on first regen; edit to switch projects)'),
|
|
32
|
+
});
|
|
33
|
+
export const secretsSchema = z.object({
|
|
34
|
+
apiKey: z.string().optional().describe('Anthropic API key'),
|
|
35
|
+
});
|
|
36
|
+
const fullConfigSchema = configSchema.merge(secretsSchema);
|
|
37
|
+
export const { defineCommand, defineTrigger, defineService, createDocumentChangeTrigger, } = createTypes({
|
|
38
|
+
configSchema: fullConfigSchema,
|
|
39
|
+
registry,
|
|
40
|
+
});
|
|
41
|
+
export { registry } from './framework.gen.js';
|
package/dist/main.d.ts
ADDED
package/dist/main.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra Studio entry point for `mastra dev` / `mastra build` / `mastra start`.
|
|
3
|
+
*
|
|
4
|
+
* Populated by codegen when `features.mastra.enabled`. Until then this file
|
|
5
|
+
* exports nothing — the `mastra:*` scripts will fail gracefully if invoked
|
|
6
|
+
* with Mastra disabled.
|
|
7
|
+
*
|
|
8
|
+
* See examples/05-ph-rupert/src/mastra/index.ts for the enabled form.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mastra Studio entry point for `mastra dev` / `mastra build` / `mastra start`.
|
|
3
|
+
*
|
|
4
|
+
* Populated by codegen when `features.mastra.enabled`. Until then this file
|
|
5
|
+
* exports nothing — the `mastra:*` scripts will fail gracefully if invoked
|
|
6
|
+
* with Mastra disabled.
|
|
7
|
+
*
|
|
8
|
+
* See examples/05-ph-rupert/src/mastra/index.ts for the enabled form.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
// @clint:end mastra-index
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ClintProjectSpec } from './types.js';
|
|
2
|
+
export declare function getSpecPath(projectDir: string): string;
|
|
3
|
+
export declare function readProjectSpec(projectDir: string): Promise<ClintProjectSpec | null>;
|
|
4
|
+
export declare function writeProjectSpec(projectDir: string, spec: ClintProjectSpec): Promise<void>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistence for `ClintProjectSpec`.
|
|
3
|
+
* Stored at `{impl-project}/.ph/ph-clint-cli/project-spec.json`.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { clintProjectSpecSchema, } from './types.js';
|
|
8
|
+
const SPEC_DIR = path.join('.ph', 'ph-clint-cli');
|
|
9
|
+
const SPEC_FILE = 'project-spec.json';
|
|
10
|
+
export function getSpecPath(projectDir) {
|
|
11
|
+
return path.join(projectDir, SPEC_DIR, SPEC_FILE);
|
|
12
|
+
}
|
|
13
|
+
function isEnoent(err) {
|
|
14
|
+
return (!!err &&
|
|
15
|
+
typeof err === 'object' &&
|
|
16
|
+
'code' in err &&
|
|
17
|
+
err.code === 'ENOENT');
|
|
18
|
+
}
|
|
19
|
+
export async function readProjectSpec(projectDir) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await fs.readFile(getSpecPath(projectDir), 'utf8');
|
|
22
|
+
return clintProjectSpecSchema.parse(JSON.parse(raw));
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
if (isEnoent(err))
|
|
26
|
+
return null;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export async function writeProjectSpec(projectDir, spec) {
|
|
31
|
+
const file = getSpecPath(projectDir);
|
|
32
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
33
|
+
await fs.writeFile(file, JSON.stringify(spec, null, 2) + '\n', 'utf8');
|
|
34
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert a `powerhouse/ph-clint-project` document's global state into a
|
|
3
|
+
* `ClintProjectSpec` that the code generator consumes.
|
|
4
|
+
*
|
|
5
|
+
* The document state is deliberately more permissive than the codegen spec:
|
|
6
|
+
* `name` is nullable in the state (so empty documents are creatable), but the
|
|
7
|
+
* generator requires a valid package name. Callers handle the `null` result
|
|
8
|
+
* (typically by skipping the regen until the user sets a name).
|
|
9
|
+
*/
|
|
10
|
+
import type { PhClintProjectGlobalState } from '@powerhousedao/ph-clint-app/document-models/ph-clint-project';
|
|
11
|
+
import { type ClintProjectSpec } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build a `ClintProjectSpec` from the document state.
|
|
14
|
+
*
|
|
15
|
+
* Returns `null` if the document has no valid package name yet — callers
|
|
16
|
+
* should wait for the user to populate it before triggering codegen.
|
|
17
|
+
*/
|
|
18
|
+
export declare function specFromDocumentState(state: PhClintProjectGlobalState): ClintProjectSpec | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { clintProjectSpecSchema, } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build a `ClintProjectSpec` from the document state.
|
|
4
|
+
*
|
|
5
|
+
* Returns `null` if the document has no valid package name yet — callers
|
|
6
|
+
* should wait for the user to populate it before triggering codegen.
|
|
7
|
+
*/
|
|
8
|
+
export function specFromDocumentState(state) {
|
|
9
|
+
if (!state.name)
|
|
10
|
+
return null;
|
|
11
|
+
// `documentTypes` is a forward-compatible field — older `ph-clint-project`
|
|
12
|
+
// document states don't declare it yet. Pull through if present, otherwise
|
|
13
|
+
// leave it to the schema default (`[]`). Codegen treats the empty list as
|
|
14
|
+
// "no registry entries" and emits an empty `defineRegistry([...])`.
|
|
15
|
+
const documentTypes = state.documentTypes ?? undefined;
|
|
16
|
+
const parsed = clintProjectSpecSchema.safeParse({
|
|
17
|
+
name: state.name,
|
|
18
|
+
scope: state.scope ?? undefined,
|
|
19
|
+
version: state.version,
|
|
20
|
+
description: state.description,
|
|
21
|
+
bin: state.bin ?? undefined,
|
|
22
|
+
features: {
|
|
23
|
+
powerhouse: { ...state.features.powerhouse },
|
|
24
|
+
mastra: { ...state.features.mastra },
|
|
25
|
+
routine: { ...state.features.routine },
|
|
26
|
+
},
|
|
27
|
+
...(Array.isArray(documentTypes) ? { documentTypes } : {}),
|
|
28
|
+
});
|
|
29
|
+
return parsed.success ? parsed.data : null;
|
|
30
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClintProjectSpec — the in-memory shape that describes a ph-clint
|
|
3
|
+
* implementation project. Persisted at
|
|
4
|
+
* `{impl-project}/.ph/ph-clint-cli/project-spec.json` and consumed by
|
|
5
|
+
* the code generator (src/codegen/).
|
|
6
|
+
*
|
|
7
|
+
* Intentionally minimal. Richer structure (services, skills, agents,
|
|
8
|
+
* triggers, document-models) gets added in later phases as each
|
|
9
|
+
* generator step actually needs it.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
export declare const clintProjectSpecSchema: z.ZodObject<{
|
|
13
|
+
name: z.ZodString;
|
|
14
|
+
scope: z.ZodOptional<z.ZodString>;
|
|
15
|
+
version: z.ZodDefault<z.ZodString>;
|
|
16
|
+
description: z.ZodDefault<z.ZodString>;
|
|
17
|
+
bin: z.ZodOptional<z.ZodString>;
|
|
18
|
+
features: z.ZodDefault<z.ZodObject<{
|
|
19
|
+
powerhouse: z.ZodDefault<z.ZodObject<{
|
|
20
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
21
|
+
switchboard: z.ZodDefault<z.ZodBoolean>;
|
|
22
|
+
connect: z.ZodDefault<z.ZodBoolean>;
|
|
23
|
+
}, z.core.$strip>>;
|
|
24
|
+
mastra: z.ZodDefault<z.ZodObject<{
|
|
25
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
26
|
+
}, z.core.$strip>>;
|
|
27
|
+
routine: z.ZodDefault<z.ZodObject<{
|
|
28
|
+
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
29
|
+
}, z.core.$strip>>;
|
|
30
|
+
}, z.core.$strip>>;
|
|
31
|
+
documentTypes: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
export type ClintProjectSpec = z.infer<typeof clintProjectSpecSchema>;
|
|
34
|
+
export type ClintProjectSpecInput = z.input<typeof clintProjectSpecSchema>;
|
|
35
|
+
/** npm package name (e.g. `@scope/foo` or `foo`). */
|
|
36
|
+
export declare function getPackageName(spec: ClintProjectSpec): string;
|
|
37
|
+
/** bin name (defaults to the bare project name). */
|
|
38
|
+
export declare function getBinName(spec: ClintProjectSpec): string;
|
|
39
|
+
/**
|
|
40
|
+
* Name of the CLI sub-folder when the project is split (Powerhouse enabled).
|
|
41
|
+
* For flat projects this is irrelevant — callers use the project root directly.
|
|
42
|
+
*/
|
|
43
|
+
export declare function getCliFolderName(spec: ClintProjectSpec): string;
|
|
44
|
+
/**
|
|
45
|
+
* Name of the reactor-package sub-folder when the project is split.
|
|
46
|
+
*/
|
|
47
|
+
export declare function getAppFolderName(spec: ClintProjectSpec): string;
|
|
48
|
+
/**
|
|
49
|
+
* Folder slug for a documentType ID. The slug is the portion after the last
|
|
50
|
+
* `/` (e.g. `powerhouse/ph-clint-project` → `ph-clint-project`). Matches the
|
|
51
|
+
* convention used by Powerhouse's reactor-package codegen — each document
|
|
52
|
+
* model lives at `document-models/<slug>/`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getDocumentTypeSlug(documentType: string): string;
|
|
55
|
+
/**
|
|
56
|
+
* PascalCase export name for a documentType ID. Powerhouse's module
|
|
57
|
+
* generator exports each module as `PascalCase(<slug>)` (e.g.
|
|
58
|
+
* `powerhouse/ph-clint-project` → `PhClintProject`). The top-level reactor
|
|
59
|
+
* package re-exports these names, so this helper gives codegen the string
|
|
60
|
+
* it needs both for the import and the `defineRegistry` entry.
|
|
61
|
+
*/
|
|
62
|
+
export declare function getDocumentTypeModuleName(documentType: string): string;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClintProjectSpec — the in-memory shape that describes a ph-clint
|
|
3
|
+
* implementation project. Persisted at
|
|
4
|
+
* `{impl-project}/.ph/ph-clint-cli/project-spec.json` and consumed by
|
|
5
|
+
* the code generator (src/codegen/).
|
|
6
|
+
*
|
|
7
|
+
* Intentionally minimal. Richer structure (services, skills, agents,
|
|
8
|
+
* triggers, document-models) gets added in later phases as each
|
|
9
|
+
* generator step actually needs it.
|
|
10
|
+
*/
|
|
11
|
+
import { z } from 'zod';
|
|
12
|
+
const powerhouseFeatureSchema = z.object({
|
|
13
|
+
enabled: z.boolean().default(false),
|
|
14
|
+
switchboard: z.boolean().default(true),
|
|
15
|
+
connect: z.boolean().default(true),
|
|
16
|
+
});
|
|
17
|
+
const mastraFeatureSchema = z.object({
|
|
18
|
+
enabled: z.boolean().default(false),
|
|
19
|
+
});
|
|
20
|
+
const routineFeatureSchema = z.object({
|
|
21
|
+
enabled: z.boolean().default(false),
|
|
22
|
+
});
|
|
23
|
+
// Zod 4 does not cascade `.default({})` through nested object schemas — a
|
|
24
|
+
// missing outer key fills in `{}`, but nested keys with their own `.default`
|
|
25
|
+
// are not re-defaulted unless the object itself re-parses. We spell out full
|
|
26
|
+
// defaults here so `clintProjectSpecSchema.parse({ name: 'x' })` yields a
|
|
27
|
+
// fully-populated feature tree.
|
|
28
|
+
const DEFAULT_POWERHOUSE = {
|
|
29
|
+
enabled: false,
|
|
30
|
+
switchboard: true,
|
|
31
|
+
connect: true,
|
|
32
|
+
};
|
|
33
|
+
const DEFAULT_MASTRA = { enabled: false };
|
|
34
|
+
const DEFAULT_ROUTINE = { enabled: false };
|
|
35
|
+
export const clintProjectSpecSchema = z.object({
|
|
36
|
+
name: z
|
|
37
|
+
.string()
|
|
38
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, 'lowercase letters, digits, hyphens only'),
|
|
39
|
+
scope: z
|
|
40
|
+
.string()
|
|
41
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/, 'lowercase letters, digits, hyphens only')
|
|
42
|
+
.optional(),
|
|
43
|
+
version: z.string().default('0.1.0'),
|
|
44
|
+
description: z.string().default(''),
|
|
45
|
+
bin: z.string().optional(),
|
|
46
|
+
features: z
|
|
47
|
+
.object({
|
|
48
|
+
powerhouse: powerhouseFeatureSchema.default(DEFAULT_POWERHOUSE),
|
|
49
|
+
mastra: mastraFeatureSchema.default(DEFAULT_MASTRA),
|
|
50
|
+
routine: routineFeatureSchema.default(DEFAULT_ROUTINE),
|
|
51
|
+
})
|
|
52
|
+
.default({
|
|
53
|
+
powerhouse: DEFAULT_POWERHOUSE,
|
|
54
|
+
mastra: DEFAULT_MASTRA,
|
|
55
|
+
routine: DEFAULT_ROUTINE,
|
|
56
|
+
}),
|
|
57
|
+
/**
|
|
58
|
+
* Document types registered with this CLI — full documentType ID strings
|
|
59
|
+
* (e.g. `powerhouse/ph-clint-project`). Drives codegen of `src/framework.gen.ts`,
|
|
60
|
+
* which emits a typed `defineRegistry([...] as const)` call so impl code
|
|
61
|
+
* gets narrowed `reactor.client.get(id, 'my-type')` access. Empty on
|
|
62
|
+
* fresh projects — populated as document models are added to the reactor
|
|
63
|
+
* package.
|
|
64
|
+
*/
|
|
65
|
+
documentTypes: z.array(z.string()).default([]),
|
|
66
|
+
});
|
|
67
|
+
/** npm package name (e.g. `@scope/foo` or `foo`). */
|
|
68
|
+
export function getPackageName(spec) {
|
|
69
|
+
return spec.scope ? `@${spec.scope}/${spec.name}` : spec.name;
|
|
70
|
+
}
|
|
71
|
+
/** bin name (defaults to the bare project name). */
|
|
72
|
+
export function getBinName(spec) {
|
|
73
|
+
return spec.bin ?? spec.name;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Name of the CLI sub-folder when the project is split (Powerhouse enabled).
|
|
77
|
+
* For flat projects this is irrelevant — callers use the project root directly.
|
|
78
|
+
*/
|
|
79
|
+
export function getCliFolderName(spec) {
|
|
80
|
+
return `${spec.name}-cli`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Name of the reactor-package sub-folder when the project is split.
|
|
84
|
+
*/
|
|
85
|
+
export function getAppFolderName(spec) {
|
|
86
|
+
return `${spec.name}-app`;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Folder slug for a documentType ID. The slug is the portion after the last
|
|
90
|
+
* `/` (e.g. `powerhouse/ph-clint-project` → `ph-clint-project`). Matches the
|
|
91
|
+
* convention used by Powerhouse's reactor-package codegen — each document
|
|
92
|
+
* model lives at `document-models/<slug>/`.
|
|
93
|
+
*/
|
|
94
|
+
export function getDocumentTypeSlug(documentType) {
|
|
95
|
+
const lastSlash = documentType.lastIndexOf('/');
|
|
96
|
+
return lastSlash >= 0 ? documentType.slice(lastSlash + 1) : documentType;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* PascalCase export name for a documentType ID. Powerhouse's module
|
|
100
|
+
* generator exports each module as `PascalCase(<slug>)` (e.g.
|
|
101
|
+
* `powerhouse/ph-clint-project` → `PhClintProject`). The top-level reactor
|
|
102
|
+
* package re-exports these names, so this helper gives codegen the string
|
|
103
|
+
* it needs both for the import and the `defineRegistry` entry.
|
|
104
|
+
*/
|
|
105
|
+
export function getDocumentTypeModuleName(documentType) {
|
|
106
|
+
const slug = getDocumentTypeSlug(documentType);
|
|
107
|
+
return slug
|
|
108
|
+
.split(/[-_]/)
|
|
109
|
+
.filter((s) => s.length > 0)
|
|
110
|
+
.map((s) => s[0].toUpperCase() + s.slice(1))
|
|
111
|
+
.join('');
|
|
112
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ClintProjectSpec } from '../spec/types.js';
|
|
2
|
+
/** Stable hash of a ClintProjectSpec (sha256 of canonical-key JSON). */
|
|
3
|
+
export declare function hashSpec(spec: ClintProjectSpec): string;
|
|
4
|
+
export declare const specChangeTrigger: import("@powerhousedao/ph-clint").Trigger<{
|
|
5
|
+
pending: number;
|
|
6
|
+
}, {
|
|
7
|
+
model: string;
|
|
8
|
+
devServicePort: number;
|
|
9
|
+
phVersion: string;
|
|
10
|
+
projectDocumentId?: string | undefined;
|
|
11
|
+
apiKey?: string | undefined;
|
|
12
|
+
}, import("@powerhousedao/ph-clint").InferRegistry<readonly [{
|
|
13
|
+
readonly version: 1;
|
|
14
|
+
readonly reducer: import("document-model").Reducer<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>;
|
|
15
|
+
readonly actions: {
|
|
16
|
+
enableRoutine: (input: import("@powerhousedao/ph-clint-app").EnableRoutineInput) => import("@powerhousedao/ph-clint-app").EnableRoutineAction;
|
|
17
|
+
disableRoutine: (input: import("@powerhousedao/ph-clint-app").DisableRoutineInput) => import("@powerhousedao/ph-clint-app").DisableRoutineAction;
|
|
18
|
+
enableMastra: (input: import("@powerhousedao/ph-clint-app").EnableMastraInput) => import("@powerhousedao/ph-clint-app").EnableMastraAction;
|
|
19
|
+
disableMastra: (input: import("@powerhousedao/ph-clint-app").DisableMastraInput) => import("@powerhousedao/ph-clint-app").DisableMastraAction;
|
|
20
|
+
enablePowerhouse: (input: import("@powerhousedao/ph-clint-app").EnablePowerhouseInput) => import("@powerhousedao/ph-clint-app").EnablePowerhouseAction;
|
|
21
|
+
setPowerhouseSwitchboard: (input: import("@powerhousedao/ph-clint-app").SetPowerhouseSwitchboardInput) => import("@powerhousedao/ph-clint-app").SetPowerhouseSwitchboardAction;
|
|
22
|
+
setPowerhouseConnect: (input: import("@powerhousedao/ph-clint-app").SetPowerhouseConnectInput) => import("@powerhousedao/ph-clint-app").SetPowerhouseConnectAction;
|
|
23
|
+
setPackageName: (input: import("@powerhousedao/ph-clint-app").SetPackageNameInput) => import("@powerhousedao/ph-clint-app").SetPackageNameAction;
|
|
24
|
+
clearBin: (input: import("@powerhousedao/ph-clint-app").ClearBinInput) => import("@powerhousedao/ph-clint-app").ClearBinAction;
|
|
25
|
+
setBin: (input: import("@powerhousedao/ph-clint-app").SetBinInput) => import("@powerhousedao/ph-clint-app").SetBinAction;
|
|
26
|
+
setDescription: (input: import("@powerhousedao/ph-clint-app").SetDescriptionInput) => import("@powerhousedao/ph-clint-app").SetDescriptionAction;
|
|
27
|
+
setVersion: (input: import("@powerhousedao/ph-clint-app").SetVersionInput) => import("@powerhousedao/ph-clint-app").SetVersionAction;
|
|
28
|
+
clearScope: (input: import("@powerhousedao/ph-clint-app").ClearScopeInput) => import("@powerhousedao/ph-clint-app").ClearScopeAction;
|
|
29
|
+
setScope: (input: import("@powerhousedao/ph-clint-app").SetScopeInput) => import("@powerhousedao/ph-clint-app").SetScopeAction;
|
|
30
|
+
setName: (name: string | {
|
|
31
|
+
name: string;
|
|
32
|
+
}) => import("document-model").SetNameAction;
|
|
33
|
+
undo: (count?: number, scope?: string) => import("document-model").UndoAction;
|
|
34
|
+
redo: (count?: number, scope?: string) => import("document-model").RedoAction;
|
|
35
|
+
prune: (start?: number, end?: number, scope?: string) => import("document-model").SchemaPruneAction;
|
|
36
|
+
loadState: <TState extends import("document-model").PHBaseState = import("document-model").PHBaseState>(state: TState & {
|
|
37
|
+
name: string;
|
|
38
|
+
}, operations: number) => import("document-model").LoadStateAction;
|
|
39
|
+
noop: (scope?: string) => import("document-model").NOOPAction;
|
|
40
|
+
};
|
|
41
|
+
readonly utils: import("document-model").DocumentModelUtils<import("@powerhousedao/ph-clint-app").PhClintProjectPHState>;
|
|
42
|
+
readonly documentModel: import("document-model").DocumentModelPHState;
|
|
43
|
+
}]>>;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `spec-change` trigger — watches the `powerhouse/ph-clint-project` document
|
|
3
|
+
* on the Reactor drive and, when its state changes, enqueues a work item
|
|
4
|
+
* that re-runs the code generator in update mode.
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. `createDocumentChangeTrigger` (from ph-clint) owns the subscription,
|
|
8
|
+
* event coalescing, doc loading, and type narrowing.
|
|
9
|
+
* 2. `documentId` resolves the target doc lazily: it prefers
|
|
10
|
+
* `config.projectDocumentId`, falling back to a drive scan for the first
|
|
11
|
+
* `powerhouse/ph-clint-project` document (and pinning the id into local
|
|
12
|
+
* config on success).
|
|
13
|
+
* 3. `onChange` converts the doc state to a `ClintProjectSpec`, hashes it,
|
|
14
|
+
* and compares against `.ph/ph-clint-cli/.last-spec-hash`. If unchanged,
|
|
15
|
+
* emits nothing. If different, returns a `function` work item that calls
|
|
16
|
+
* `generateProject(spec, workdir, 'update')` and writes the new hash.
|
|
17
|
+
*/
|
|
18
|
+
import crypto from 'node:crypto';
|
|
19
|
+
import fs from 'node:fs/promises';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { createDocumentChangeTrigger } from '../framework.js';
|
|
22
|
+
import { generateProject } from '../codegen/index.js';
|
|
23
|
+
import { specFromDocumentState } from '../spec/from-document.js';
|
|
24
|
+
const DOCUMENT_TYPE = 'powerhouse/ph-clint-project';
|
|
25
|
+
const HASH_DIR = path.join('.ph', 'ph-clint-cli');
|
|
26
|
+
const HASH_FILE = '.last-spec-hash';
|
|
27
|
+
const TAG = '[spec-change]';
|
|
28
|
+
function getHashPath(targetDir) {
|
|
29
|
+
return path.join(targetDir, HASH_DIR, HASH_FILE);
|
|
30
|
+
}
|
|
31
|
+
function isEnoent(err) {
|
|
32
|
+
return (!!err &&
|
|
33
|
+
typeof err === 'object' &&
|
|
34
|
+
'code' in err &&
|
|
35
|
+
err.code === 'ENOENT');
|
|
36
|
+
}
|
|
37
|
+
async function readLastSpecHash(targetDir) {
|
|
38
|
+
try {
|
|
39
|
+
return (await fs.readFile(getHashPath(targetDir), 'utf8')).trim() || null;
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (isEnoent(err))
|
|
43
|
+
return null;
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function writeLastSpecHash(targetDir, hash) {
|
|
48
|
+
const file = getHashPath(targetDir);
|
|
49
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
50
|
+
await fs.writeFile(file, hash + '\n', 'utf8');
|
|
51
|
+
}
|
|
52
|
+
/** Stable hash of a ClintProjectSpec (sha256 of canonical-key JSON). */
|
|
53
|
+
export function hashSpec(spec) {
|
|
54
|
+
return crypto
|
|
55
|
+
.createHash('sha256')
|
|
56
|
+
.update(canonicalJson(spec), 'utf8')
|
|
57
|
+
.digest('hex');
|
|
58
|
+
}
|
|
59
|
+
function canonicalJson(value) {
|
|
60
|
+
if (value === null || typeof value !== 'object')
|
|
61
|
+
return JSON.stringify(value);
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
return '[' + value.map(canonicalJson).join(',') + ']';
|
|
64
|
+
}
|
|
65
|
+
const obj = value;
|
|
66
|
+
const keys = Object.keys(obj).sort();
|
|
67
|
+
return ('{' +
|
|
68
|
+
keys
|
|
69
|
+
.map((k) => JSON.stringify(k) + ':' + canonicalJson(obj[k]))
|
|
70
|
+
.join(',') +
|
|
71
|
+
'}');
|
|
72
|
+
}
|
|
73
|
+
/** Find the first ph-clint-project document on the default drive. */
|
|
74
|
+
async function findProjectDocumentId(client, driveId) {
|
|
75
|
+
try {
|
|
76
|
+
const children = await client.getChildren(driveId);
|
|
77
|
+
const results = (children?.results ?? []);
|
|
78
|
+
for (const node of results) {
|
|
79
|
+
if (node.header?.documentType === DOCUMENT_TYPE && node.header.id) {
|
|
80
|
+
return node.header.id;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// fall through
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
export const specChangeTrigger = createDocumentChangeTrigger({
|
|
90
|
+
id: 'spec-change',
|
|
91
|
+
documentType: DOCUMENT_TYPE,
|
|
92
|
+
// Resolve the target id lazily so we can also do first-time discovery and
|
|
93
|
+
// persist the discovered id into local config.
|
|
94
|
+
documentId: async (ctx) => {
|
|
95
|
+
const configured = ctx.context.config.projectDocumentId &&
|
|
96
|
+
ctx.context.config.projectDocumentId.length > 0
|
|
97
|
+
? ctx.context.config.projectDocumentId
|
|
98
|
+
: undefined;
|
|
99
|
+
if (configured)
|
|
100
|
+
return configured;
|
|
101
|
+
const reactor = await ctx.reactor();
|
|
102
|
+
if (!reactor)
|
|
103
|
+
return undefined;
|
|
104
|
+
const discovered = await findProjectDocumentId(reactor.client, reactor.driveId);
|
|
105
|
+
if (!discovered) {
|
|
106
|
+
ctx.context.log?.debug(`${TAG} no ${DOCUMENT_TYPE} document found on drive ${reactor.driveId}`);
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
// Persist into local config + mutate in-memory so future ticks skip discovery.
|
|
110
|
+
try {
|
|
111
|
+
const current = await ctx.context.workspace.loadLocalConfig({});
|
|
112
|
+
await ctx.context.workspace.storeLocalConfig({
|
|
113
|
+
...current,
|
|
114
|
+
projectDocumentId: discovered,
|
|
115
|
+
});
|
|
116
|
+
ctx.context.config.projectDocumentId = discovered;
|
|
117
|
+
ctx.context.log?.info(`${TAG} pinned projectDocumentId=${discovered} in ${ctx.context.workspace.getLocalConfigPath()}`);
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
ctx.context.log?.warn(`${TAG} failed to persist projectDocumentId: ${err instanceof Error ? err.message : String(err)}`);
|
|
121
|
+
}
|
|
122
|
+
return discovered;
|
|
123
|
+
},
|
|
124
|
+
async onChange(doc, ctx) {
|
|
125
|
+
const log = ctx.context.log;
|
|
126
|
+
const spec = specFromDocumentState(doc.state.global);
|
|
127
|
+
if (!spec) {
|
|
128
|
+
log?.debug(`${TAG} document ${doc.header.id} state did not validate against ClintProjectSpec, skipping`);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
const targetDir = ctx.context.workdir;
|
|
132
|
+
const nextHash = hashSpec(spec);
|
|
133
|
+
const prevHash = await readLastSpecHash(targetDir);
|
|
134
|
+
if (nextHash === prevHash) {
|
|
135
|
+
log?.debug(`${TAG} spec unchanged (hash=${nextHash.slice(0, 12)}…), nothing to do`);
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
log?.debug(`${TAG} spec changed: ${prevHash ? prevHash.slice(0, 12) + '…' : '<none>'} → ${nextHash.slice(0, 12)}…`);
|
|
139
|
+
return {
|
|
140
|
+
type: 'function',
|
|
141
|
+
params: {
|
|
142
|
+
fn: async () => {
|
|
143
|
+
log?.info(`${TAG} regenerating project at ${targetDir} (doc=${doc.header.id})`);
|
|
144
|
+
const t0 = Date.now();
|
|
145
|
+
const warnings = [];
|
|
146
|
+
const result = await generateProject({
|
|
147
|
+
targetDir,
|
|
148
|
+
spec,
|
|
149
|
+
mode: 'update',
|
|
150
|
+
onWarn: (m) => warnings.push(m),
|
|
151
|
+
});
|
|
152
|
+
await writeLastSpecHash(targetDir, nextHash);
|
|
153
|
+
const elapsed = Date.now() - t0;
|
|
154
|
+
if (log) {
|
|
155
|
+
if (result.migrated)
|
|
156
|
+
log.info(`${TAG} migrated flat → split layout`);
|
|
157
|
+
log.info(`${TAG} regenerated in ${elapsed}ms: ${result.files.length} written, ${result.skipped.length} skipped, ${result.deleted.length} deleted`);
|
|
158
|
+
for (const w of warnings)
|
|
159
|
+
log.warn(`${TAG} ${w}`);
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
callbacks: {
|
|
164
|
+
onFailure: (err) => {
|
|
165
|
+
log?.error(`${TAG} regen failed: ${err.message}`);
|
|
166
|
+
log?.debug(`${TAG} stack: ${err.stack ?? '<no stack>'}`);
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
},
|
|
171
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@powerhousedao/ph-clint-cli",
|
|
3
|
+
"version": "0.1.0-dev.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"description": "ph-clint CLI — scaffolds and maintains ph-clint implementation projects. Itself built with ph-clint from the ph-clint-tpl template.",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"ph-clint": "./dist/main.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist",
|
|
14
|
+
"prompts"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build:skills": "tsx scripts/build-skills.ts",
|
|
18
|
+
"build": "pnpm build:skills && tsc",
|
|
19
|
+
"dev": "tsx src/main.ts",
|
|
20
|
+
"start": "node dist/main.js",
|
|
21
|
+
"test": "NODE_OPTIONS='--experimental-vm-modules' jest --detectOpenHandles",
|
|
22
|
+
"lint": "eslint src",
|
|
23
|
+
"mastra:dev": "mastra dev",
|
|
24
|
+
"mastra:build": "mastra build",
|
|
25
|
+
"mastra:start": "mastra start",
|
|
26
|
+
"publish:npm": "pnpm build && pnpm publish --no-git-checks"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@powerhousedao/ph-clint": "^0.1.0-dev.0",
|
|
30
|
+
"@powerhousedao/ph-clint-app": "^0.1.0-dev.0",
|
|
31
|
+
"@mastra/core": "^1.22.0",
|
|
32
|
+
"@mastra/libsql": "^1.7.4",
|
|
33
|
+
"@mastra/memory": "^1.13.1",
|
|
34
|
+
"@powerhousedao/reactor": "6.0.0-dev.170",
|
|
35
|
+
"@powerhousedao/reactor-api": "6.0.0-dev.170",
|
|
36
|
+
"@powerhousedao/shared": "6.0.0-dev.170",
|
|
37
|
+
"@electric-sql/pglite": "0.3.15",
|
|
38
|
+
"document-model": "6.0.0-dev.170",
|
|
39
|
+
"kysely": "^0.27.0",
|
|
40
|
+
"kysely-pglite-dialect": "^1.3.0",
|
|
41
|
+
"zod": "^4.3.6"
|
|
42
|
+
},
|
|
43
|
+
"pnpm": {
|
|
44
|
+
"overrides": {
|
|
45
|
+
"mastra>@mastra/deployer": "1.22.0"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@jest/globals": "^30.2.0",
|
|
50
|
+
"@types/node": "^25.5.2",
|
|
51
|
+
"jest": "^30.2.0",
|
|
52
|
+
"@powerhousedao/ph-clint-dev": "^0.1.0-dev.0",
|
|
53
|
+
"ts-jest": "^29.4.6",
|
|
54
|
+
"tsx": "^4.19.0",
|
|
55
|
+
"typescript": "^6.0.2"
|
|
56
|
+
},
|
|
57
|
+
"engines": {
|
|
58
|
+
"node": ">=22.13.0"
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Agent Base System Prompt
|
|
2
|
+
|
|
3
|
+
You are {{agentName}}, an agent available through the {{cliName}} CLI.
|
|
4
|
+
|
|
5
|
+
## Capabilities
|
|
6
|
+
|
|
7
|
+
This base profile is a minimal placeholder. Extend it with project-specific
|
|
8
|
+
context, workflows, and domain knowledge. Additional sections listed in the
|
|
9
|
+
agent's `sections` array are concatenated after this file at build time.
|
|
10
|
+
|
|
11
|
+
See `examples/05-ph-rupert/prompts/agent-profiles/AgentBase.md` for a more
|
|
12
|
+
complete example covering Powerhouse document system fundamentals,
|
|
13
|
+
collaboration documents (inbox, WBS), and task management guidelines.
|
|
File without changes
|
|
File without changes
|