@1mancompany/onemancompany 0.7.80 → 0.7.85
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/bin/cli.js
CHANGED
|
@@ -142,6 +142,19 @@ function runShell(cmd, opts = {}) {
|
|
|
142
142
|
return execSync(cmd, { stdio: "inherit", shell: true, ...opts });
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
// Read the installed app's version from installDir/pyproject.toml.
|
|
146
|
+
// Returns null if the file is missing, unreadable, or has no version line.
|
|
147
|
+
// Exported via global for unit tests; the CLI itself uses it directly.
|
|
148
|
+
function readAppVersion(installDir) {
|
|
149
|
+
try {
|
|
150
|
+
const pyproject = fs.readFileSync(path.join(installDir, "pyproject.toml"), "utf-8");
|
|
151
|
+
const verMatch = pyproject.match(/^version\s*=\s*"([^"]+)"/m);
|
|
152
|
+
return verMatch ? verMatch[1] : null;
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
145
158
|
// ── UV installer ────────────────────────────────────────────────────────────
|
|
146
159
|
function ensureUV() {
|
|
147
160
|
if (commandExists("uv")) {
|
|
@@ -219,8 +232,8 @@ async function main() {
|
|
|
219
232
|
${cyan("OneManCompany")} — The Agent Operating System for One Man Companies
|
|
220
233
|
|
|
221
234
|
${green("Usage:")}
|
|
222
|
-
npx @1mancompany/onemancompany Start (runs in background)
|
|
223
|
-
npx @1mancompany/onemancompany --update
|
|
235
|
+
npx @1mancompany/onemancompany Start (runs in background; refreshes source by default)
|
|
236
|
+
npx @1mancompany/onemancompany --no-update Start without refreshing source (keep local edits)
|
|
224
237
|
npx @1mancompany/onemancompany --debug Start with logs (Ctrl+C to stop)
|
|
225
238
|
npx @1mancompany/onemancompany stop Stop background service
|
|
226
239
|
npx @1mancompany/onemancompany init Re-run setup process (interactive)
|
|
@@ -233,7 +246,7 @@ ${green("Usage:")}
|
|
|
233
246
|
${green("Options:")}
|
|
234
247
|
--dir <path> Install directory (default: ./OneManCompany)
|
|
235
248
|
--port <port> Server port (default: 8000)
|
|
236
|
-
--update
|
|
249
|
+
--no-update Skip refreshing bundled code (default: refresh src/, frontend/, pyproject.toml, uv.lock on every run; company/ and config.yaml are always preserved)
|
|
237
250
|
--debug Run in foreground with logs (default: background)
|
|
238
251
|
--help, -h Show this help
|
|
239
252
|
|
|
@@ -325,35 +338,58 @@ ${green("What gets installed automatically:")}
|
|
|
325
338
|
}
|
|
326
339
|
|
|
327
340
|
// ── Install or update ──────────────────────────────────────────────────
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
341
|
+
// Two classes of bundled items, treated differently on subsequent runs:
|
|
342
|
+
// * CODE — owned by the project, refreshed on every run so `@dev` actually
|
|
343
|
+
// means "give me the latest code." Safe to overwrite because users
|
|
344
|
+
// shouldn't be editing these directly.
|
|
345
|
+
// * USER-OWNED — bootstrapped on first install, NEVER overwritten on
|
|
346
|
+
// update. `company/` holds workflows / SOPs / founding-employee profiles
|
|
347
|
+
// that users routinely edit; `config.yaml` holds local config. Blowing
|
|
348
|
+
// these away on every `npx` run would silently destroy work.
|
|
349
|
+
// Pass --no-update to skip refreshing CODE too (e.g. when debugging local
|
|
350
|
+
// patches in installDir/src). --update is a silent no-op (was the old
|
|
351
|
+
// opt-in flag; current behavior matches what it used to enable).
|
|
352
|
+
const CODE_ITEMS = ["src", "frontend", "pyproject.toml", "uv.lock"];
|
|
353
|
+
const USER_OWNED_ITEMS = ["company", "config.yaml"];
|
|
354
|
+
const wantNoUpdate = passthrough.includes("--no-update");
|
|
355
|
+
|
|
356
|
+
function copyItems(items, destRoot, { overwrite }) {
|
|
334
357
|
for (const item of items) {
|
|
335
358
|
const src = path.join(npmPkgRoot, item);
|
|
336
359
|
const dest = path.join(destRoot, item);
|
|
337
|
-
if (fs.existsSync(src))
|
|
338
|
-
|
|
360
|
+
if (!fs.existsSync(src)) continue;
|
|
361
|
+
if (fs.existsSync(dest)) {
|
|
362
|
+
if (!overwrite) continue;
|
|
363
|
+
// Copy to sibling temp then rename, so an interrupted run can't leave
|
|
364
|
+
// installDir/<item> in a half-deleted state.
|
|
365
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
366
|
+
if (fs.existsSync(tmp)) fs.rmSync(tmp, { recursive: true, force: true });
|
|
367
|
+
fs.cpSync(src, tmp, { recursive: true });
|
|
368
|
+
fs.rmSync(dest, { recursive: true, force: true });
|
|
369
|
+
fs.renameSync(tmp, dest);
|
|
370
|
+
} else {
|
|
339
371
|
fs.cpSync(src, dest, { recursive: true });
|
|
340
372
|
}
|
|
341
373
|
}
|
|
342
374
|
}
|
|
343
375
|
|
|
344
376
|
if (fs.existsSync(installDir)) {
|
|
345
|
-
if (
|
|
346
|
-
info(`
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
377
|
+
if (wantNoUpdate) {
|
|
378
|
+
info(`Using existing installation at ${installDir} (--no-update)`);
|
|
379
|
+
} else if (sourceIsBundled) {
|
|
380
|
+
info(`Updating code to v${cliVersion}... (user-owned files in company/ and config.yaml are preserved)`);
|
|
381
|
+
copyItems(CODE_ITEMS, installDir, { overwrite: true });
|
|
382
|
+
// Bootstrap user-owned items only if they were never created (e.g.
|
|
383
|
+
// partial-install recovery). Existing files are left untouched.
|
|
384
|
+
copyItems(USER_OWNED_ITEMS, installDir, { overwrite: false });
|
|
350
385
|
} else {
|
|
351
|
-
|
|
386
|
+
warn(`Bundled source not found in npm package — keeping existing files at ${installDir}. Try reinstalling: npm cache clean --force && npx --yes @1mancompany/onemancompany@<version>`);
|
|
352
387
|
}
|
|
353
388
|
} else if (sourceIsBundled) {
|
|
354
389
|
info(`Installing OneManCompany v${cliVersion} into ${installDir}...`);
|
|
355
390
|
fs.mkdirSync(installDir, { recursive: true });
|
|
356
|
-
copyItems(
|
|
391
|
+
copyItems(CODE_ITEMS, installDir, { overwrite: true });
|
|
392
|
+
copyItems(USER_OWNED_ITEMS, installDir, { overwrite: true });
|
|
357
393
|
} else {
|
|
358
394
|
// Fallback: no bundled source (broken package?) — clone from git
|
|
359
395
|
info(`Cloning OneManCompany into ${installDir}...`);
|
|
@@ -361,9 +397,19 @@ ${green("What gets installed automatically:")}
|
|
|
361
397
|
run(`git clone --depth 1 ${REPO_URL} "${installDir}"`, { env: cloneEnv });
|
|
362
398
|
}
|
|
363
399
|
|
|
400
|
+
// ── Read the *installed* app version (the only honest source for the banner) —
|
|
401
|
+
// Do NOT use cliVersion as a fallback: when --no-update is set, the npm CLI
|
|
402
|
+
// and the installed app can be on different versions. Showing the wrong
|
|
403
|
+
// number is worse than showing "unknown".
|
|
404
|
+
const installedAppVersion = readAppVersion(installDir);
|
|
405
|
+
if (!installedAppVersion) {
|
|
406
|
+
warn(`Could not read app version from ${path.join(installDir, "pyproject.toml")} — banner shows "unknown"`);
|
|
407
|
+
}
|
|
408
|
+
const appVersion = installedAppVersion || "unknown";
|
|
409
|
+
|
|
364
410
|
// ── Banner (after real version is known) ───────────────────────────
|
|
365
411
|
console.log();
|
|
366
|
-
const verTag = `v${
|
|
412
|
+
const verTag = `v${appVersion}`;
|
|
367
413
|
const title = `OneManCompany — AI Company OS ${verTag}`;
|
|
368
414
|
const pad = Math.max(0, 44 - title.length);
|
|
369
415
|
console.log(cyan("╔═══════════════════════════════════════════════╗"));
|
|
@@ -534,7 +580,10 @@ ${green("What gets installed automatically:")}
|
|
|
534
580
|
|
|
535
581
|
// Start server
|
|
536
582
|
const debugMode = passthrough.includes("--debug");
|
|
537
|
-
|
|
583
|
+
// CLI-only flags must be stripped before forwarding to onemancompany.main,
|
|
584
|
+
// otherwise argparse there will reject the unknown argument.
|
|
585
|
+
const CLI_ONLY_FLAGS = new Set(["--debug", "--update", "--no-update"]);
|
|
586
|
+
const launchArgs = passthrough.filter((a) => !CLI_ONLY_FLAGS.has(a));
|
|
538
587
|
|
|
539
588
|
// Build env: pass OMC_DEBUG=1 in debug mode
|
|
540
589
|
const childEnv = { ...process.env };
|
|
@@ -542,7 +591,7 @@ ${green("What gets installed automatically:")}
|
|
|
542
591
|
|
|
543
592
|
if (debugMode) {
|
|
544
593
|
// ── Foreground mode: show logs, Ctrl+C to kill ──────────────────
|
|
545
|
-
info(`Starting OneManCompany v${
|
|
594
|
+
info(`Starting OneManCompany v${appVersion} in debug mode (Ctrl+C to stop)...\n`);
|
|
546
595
|
const child = spawn(pythonBin, ["-m", "onemancompany.main", ...launchArgs], {
|
|
547
596
|
cwd: installDir,
|
|
548
597
|
stdio: "inherit",
|
|
@@ -558,7 +607,7 @@ ${green("What gets installed automatically:")}
|
|
|
558
607
|
process.on("SIGTERM", () => { child.kill("SIGTERM"); });
|
|
559
608
|
} else {
|
|
560
609
|
// ── Background mode: detach and exit CLI ────────────────────────
|
|
561
|
-
info(`Starting OneManCompany v${
|
|
610
|
+
info(`Starting OneManCompany v${appVersion} in background...`);
|
|
562
611
|
const logFile = path.join(installDir, ".onemancompany", "server.log");
|
|
563
612
|
// Ensure log directory exists
|
|
564
613
|
const logDir = path.dirname(logFile);
|
|
@@ -581,7 +630,7 @@ ${green("What gets installed automatically:")}
|
|
|
581
630
|
await new Promise((r) => setTimeout(r, 5000));
|
|
582
631
|
if (isProcessRunning(child.pid)) {
|
|
583
632
|
console.log();
|
|
584
|
-
console.log(green(` ✓ OneManCompany v${
|
|
633
|
+
console.log(green(` ✓ OneManCompany v${appVersion} is running!`));
|
|
585
634
|
console.log();
|
|
586
635
|
console.log(` ${cyan("→")} Open ${cyan("http://localhost:8000")} in your browser`);
|
|
587
636
|
console.log(` ${dim(" Logs:")} ${logFile}`);
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -86,6 +86,29 @@ async def create_product_tool(
|
|
|
86
86
|
return f"Error: {e}"
|
|
87
87
|
|
|
88
88
|
|
|
89
|
+
@tool
|
|
90
|
+
async def add_product_key_result(
|
|
91
|
+
product_slug: str,
|
|
92
|
+
title: str,
|
|
93
|
+
target: float,
|
|
94
|
+
unit: str = "",
|
|
95
|
+
) -> str:
|
|
96
|
+
"""Add a Key Result to an existing product (use during product planning).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
product_slug: The product slug (e.g. "omc-website")
|
|
100
|
+
title: KR title (e.g. "DAU达到1000")
|
|
101
|
+
target: Numeric target value
|
|
102
|
+
unit: Unit of measurement (e.g. "users", "seconds")
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
kr = prod.add_key_result(product_slug, title=title, target=target, unit=unit)
|
|
106
|
+
logger.debug("add_product_key_result: {} for {}", kr["id"], product_slug)
|
|
107
|
+
return f"Added key result '{title}' (target={target} {unit}) to {product_slug}"
|
|
108
|
+
except (ValueError, FileNotFoundError) as e:
|
|
109
|
+
return f"Error: {e}"
|
|
110
|
+
|
|
111
|
+
|
|
89
112
|
@tool
|
|
90
113
|
async def create_product_issue(
|
|
91
114
|
product_slug: str,
|
|
@@ -789,6 +812,7 @@ async def transfer_product_ownership_tool(
|
|
|
789
812
|
|
|
790
813
|
PRODUCT_TOOLS = [
|
|
791
814
|
create_product_tool,
|
|
815
|
+
add_product_key_result,
|
|
792
816
|
create_product_issue,
|
|
793
817
|
update_product_issue,
|
|
794
818
|
close_product_issue,
|
|
@@ -7444,6 +7444,33 @@ async def api_start_product_planning(slug: str) -> dict:
|
|
|
7444
7444
|
product_slug=slug,
|
|
7445
7445
|
product_id=product["id"],
|
|
7446
7446
|
)
|
|
7447
|
+
|
|
7448
|
+
# Kickoff: persist a synthetic system message and dispatch to the EA adapter
|
|
7449
|
+
# so the agent posts an opening message and can begin creating KRs/issues.
|
|
7450
|
+
# Use SYSTEM_SENDER so the UI doesn't render this as the CEO speaking — the
|
|
7451
|
+
# prompt builder uses `role` only as a label, so EA still gets a coherent prompt.
|
|
7452
|
+
kickoff_text = f"开始为产品「{product['name']}」做规划。请先帮我梳理目标和关键结果。"
|
|
7453
|
+
try:
|
|
7454
|
+
kickoff_msg = await conversation_service.send_message(
|
|
7455
|
+
conv.id, sender=SYSTEM_SENDER, role="System", text=kickoff_text,
|
|
7456
|
+
)
|
|
7457
|
+
task = asyncio.create_task(_dispatch_conversation_to_adapter(conv.id, kickoff_msg))
|
|
7458
|
+
_active_adapter_tasks.add(task)
|
|
7459
|
+
_active_adapter_by_conv[conv.id] = task
|
|
7460
|
+
def _cleanup(t, _cid=conv.id):
|
|
7461
|
+
_active_adapter_tasks.discard(t)
|
|
7462
|
+
_active_adapter_by_conv.pop(_cid, None)
|
|
7463
|
+
task.add_done_callback(_cleanup)
|
|
7464
|
+
except Exception:
|
|
7465
|
+
logger.exception("[product_planning] failed to kick off EA for product {}", slug)
|
|
7466
|
+
try:
|
|
7467
|
+
await conversation_service.send_message(
|
|
7468
|
+
conv.id, sender=SYSTEM_SENDER, role="System",
|
|
7469
|
+
text="(Failed to start the planning agent. Please send a message to retry.)",
|
|
7470
|
+
)
|
|
7471
|
+
except Exception:
|
|
7472
|
+
logger.exception("[product_planning] failed to send kickoff-failure notice for {}", conv.id)
|
|
7473
|
+
|
|
7447
7474
|
return {"conversation_id": conv.id, "existing": False}
|
|
7448
7475
|
|
|
7449
7476
|
|
|
@@ -129,6 +129,11 @@ def _build_conversation_prompt(
|
|
|
129
129
|
lines.append(shared_prompt)
|
|
130
130
|
elif conversation.type == ConversationType.CEO_INBOX:
|
|
131
131
|
lines.append("The CEO is responding to your request. Answer their questions.")
|
|
132
|
+
elif (
|
|
133
|
+
conversation.type == ConversationType.PRODUCT
|
|
134
|
+
or conversation.type == ConversationType.PRODUCT.value
|
|
135
|
+
):
|
|
136
|
+
lines.append(_build_product_planning_prompt(conversation))
|
|
132
137
|
elif conversation.type == ConversationType.EA_CHAT:
|
|
133
138
|
lines.append(
|
|
134
139
|
"This is a direct chat with the CEO. You are their EA (Executive Assistant).\n"
|
|
@@ -158,6 +163,55 @@ def _build_conversation_prompt(
|
|
|
158
163
|
return "\n".join(lines)
|
|
159
164
|
|
|
160
165
|
|
|
166
|
+
def _build_product_planning_prompt(conversation: Conversation) -> str:
|
|
167
|
+
"""Build planning instructions + current product context for PRODUCT conversations."""
|
|
168
|
+
from onemancompany.core import product as prod
|
|
169
|
+
from onemancompany.core.models import IssueStatus
|
|
170
|
+
|
|
171
|
+
slug = (conversation.metadata or {}).get("product_slug", "")
|
|
172
|
+
product = prod.load_product(slug) if slug else None
|
|
173
|
+
if not product:
|
|
174
|
+
return (
|
|
175
|
+
"This is a product planning conversation, but the product could not be loaded. "
|
|
176
|
+
f"Slug: {slug!r}. Ask the CEO to clarify."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
parts = [
|
|
180
|
+
"This is a product planning conversation with the CEO. You are the EA (Executive Assistant).",
|
|
181
|
+
"Your job:",
|
|
182
|
+
" 1. Clarify the product's objective and success metrics through targeted questions.",
|
|
183
|
+
" 2. Use add_product_key_result(product_slug, title, target, unit) to record measurable KRs.",
|
|
184
|
+
" 3. Use create_product_issue(product_slug, title, description, priority) to record concrete work items.",
|
|
185
|
+
" 4. Keep questions concise and one at a time. Confirm before creating each KR/issue.",
|
|
186
|
+
"",
|
|
187
|
+
f"## Current product: {product['name']} (slug: {slug})",
|
|
188
|
+
]
|
|
189
|
+
if product.get("description"):
|
|
190
|
+
parts.append(f"Description: {product['description']}")
|
|
191
|
+
|
|
192
|
+
krs = product.get("key_results", []) or []
|
|
193
|
+
if krs:
|
|
194
|
+
parts.append("\n### Existing Key Results")
|
|
195
|
+
for kr in krs:
|
|
196
|
+
target = kr.get("target", 0)
|
|
197
|
+
current = kr.get("current", 0)
|
|
198
|
+
parts.append(f"- {kr.get('title','')}: {current}/{target} {kr.get('unit','')}".rstrip())
|
|
199
|
+
else:
|
|
200
|
+
parts.append("\n### Existing Key Results: (none yet — propose some after clarifying with CEO)")
|
|
201
|
+
|
|
202
|
+
issues = prod.list_issues(slug) or []
|
|
203
|
+
terminal = {IssueStatus.DONE.value, IssueStatus.RELEASED.value}
|
|
204
|
+
open_issues = [i for i in issues if i.get("status") not in terminal]
|
|
205
|
+
if open_issues:
|
|
206
|
+
parts.append(f"\n### Open Issues ({len(open_issues)})")
|
|
207
|
+
for issue in open_issues[:10]:
|
|
208
|
+
parts.append(f"- [{issue.get('priority','?')}] {issue.get('title','')} ({issue.get('id','')})")
|
|
209
|
+
else:
|
|
210
|
+
parts.append("\n### Open Issues: (none yet)")
|
|
211
|
+
|
|
212
|
+
return "\n".join(parts)
|
|
213
|
+
|
|
214
|
+
|
|
161
215
|
def _load_oneonone_workspace_shared_prompt() -> str:
|
|
162
216
|
"""Load workspace policy prompt for one-on-one from shared_prompts."""
|
|
163
217
|
from onemancompany.core.config import SHARED_PROMPTS_DIR, SOURCE_ROOT, read_text_utf
|